diff options
Diffstat (limited to 'server/lib/emailer.ts')
-rw-r--r-- | server/lib/emailer.ts | 440 |
1 files changed, 8 insertions, 432 deletions
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 458214f88..6bb61484b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -1,20 +1,15 @@ | |||
1 | import { readFileSync } from 'fs-extra' | 1 | import { readFileSync } from 'fs-extra' |
2 | import { merge } from 'lodash' | 2 | import { isArray, merge } from 'lodash' |
3 | import { createTransport, Transporter } from 'nodemailer' | 3 | import { createTransport, Transporter } from 'nodemailer' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | 5 | import { EmailPayload } from '@shared/models' |
6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | ||
8 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | ||
9 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' | 6 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
10 | import { isTestInstance, root } from '../helpers/core-utils' | 7 | import { isTestInstance, root } from '../helpers/core-utils' |
11 | import { bunyanLogger, logger } from '../helpers/logger' | 8 | import { bunyanLogger, logger } from '../helpers/logger' |
12 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 9 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
13 | import { WEBSERVER } from '../initializers/constants' | 10 | import { WEBSERVER } from '../initializers/constants' |
14 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' | 11 | import { MUser } from '../types/models' |
15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | ||
16 | import { JobQueue } from './job-queue' | 12 | import { JobQueue } from './job-queue' |
17 | import { toSafeHtml } from '../helpers/markdown' | ||
18 | 13 | ||
19 | const Email = require('email-templates') | 14 | const Email = require('email-templates') |
20 | 15 | ||
@@ -59,429 +54,6 @@ class Emailer { | |||
59 | } | 54 | } |
60 | } | 55 | } |
61 | 56 | ||
62 | addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) { | ||
63 | const channelName = video.VideoChannel.getDisplayName() | ||
64 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | ||
65 | |||
66 | const emailPayload: EmailPayload = { | ||
67 | to, | ||
68 | subject: channelName + ' just published a new video', | ||
69 | text: `Your subscription ${channelName} just published a new video: "${video.name}".`, | ||
70 | locals: { | ||
71 | title: 'New content ', | ||
72 | action: { | ||
73 | text: 'View video', | ||
74 | url: videoUrl | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | |||
79 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
80 | } | ||
81 | |||
82 | addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { | ||
83 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() | ||
84 | |||
85 | const emailPayload: EmailPayload = { | ||
86 | template: 'follower-on-channel', | ||
87 | to, | ||
88 | subject: `New follower on your channel ${followingName}`, | ||
89 | locals: { | ||
90 | followerName: actorFollow.ActorFollower.Account.getDisplayName(), | ||
91 | followerUrl: actorFollow.ActorFollower.url, | ||
92 | followingName, | ||
93 | followingUrl: actorFollow.ActorFollowing.url, | ||
94 | followType | ||
95 | } | ||
96 | } | ||
97 | |||
98 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
99 | } | ||
100 | |||
101 | addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { | ||
102 | const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' | ||
103 | |||
104 | const emailPayload: EmailPayload = { | ||
105 | to, | ||
106 | subject: 'New instance follower', | ||
107 | text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`, | ||
108 | locals: { | ||
109 | title: 'New instance follower', | ||
110 | action: { | ||
111 | text: 'Review followers', | ||
112 | url: WEBSERVER.URL + '/admin/follows/followers-list' | ||
113 | } | ||
114 | } | ||
115 | } | ||
116 | |||
117 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
118 | } | ||
119 | |||
120 | addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { | ||
121 | const instanceUrl = actorFollow.ActorFollowing.url | ||
122 | const emailPayload: EmailPayload = { | ||
123 | to, | ||
124 | subject: 'Auto instance following', | ||
125 | text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.` | ||
126 | } | ||
127 | |||
128 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
129 | } | ||
130 | |||
131 | myVideoPublishedNotification (to: string[], video: MVideo) { | ||
132 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | ||
133 | |||
134 | const emailPayload: EmailPayload = { | ||
135 | to, | ||
136 | subject: `Your video ${video.name} has been published`, | ||
137 | text: `Your video "${video.name}" has been published.`, | ||
138 | locals: { | ||
139 | title: 'You video is live', | ||
140 | action: { | ||
141 | text: 'View video', | ||
142 | url: videoUrl | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | |||
147 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
148 | } | ||
149 | |||
150 | myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { | ||
151 | const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() | ||
152 | |||
153 | const emailPayload: EmailPayload = { | ||
154 | to, | ||
155 | subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`, | ||
156 | text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`, | ||
157 | locals: { | ||
158 | title: 'Import complete', | ||
159 | action: { | ||
160 | text: 'View video', | ||
161 | url: videoUrl | ||
162 | } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
167 | } | ||
168 | |||
169 | myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { | ||
170 | const importUrl = WEBSERVER.URL + '/my-library/video-imports' | ||
171 | |||
172 | const text = | ||
173 | `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` + | ||
174 | '\n\n' + | ||
175 | `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.` | ||
176 | |||
177 | const emailPayload: EmailPayload = { | ||
178 | to, | ||
179 | subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`, | ||
180 | text, | ||
181 | locals: { | ||
182 | title: 'Import failed', | ||
183 | action: { | ||
184 | text: 'Review imports', | ||
185 | url: importUrl | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | |||
190 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
191 | } | ||
192 | |||
193 | addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { | ||
194 | const video = comment.Video | ||
195 | const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() | ||
196 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() | ||
197 | const commentHtml = toSafeHtml(comment.text) | ||
198 | |||
199 | const emailPayload: EmailPayload = { | ||
200 | template: 'video-comment-new', | ||
201 | to, | ||
202 | subject: 'New comment on your video ' + video.name, | ||
203 | locals: { | ||
204 | accountName: comment.Account.getDisplayName(), | ||
205 | accountUrl: comment.Account.Actor.url, | ||
206 | comment, | ||
207 | commentHtml, | ||
208 | video, | ||
209 | videoUrl, | ||
210 | action: { | ||
211 | text: 'View comment', | ||
212 | url: commentUrl | ||
213 | } | ||
214 | } | ||
215 | } | ||
216 | |||
217 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
218 | } | ||
219 | |||
220 | addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { | ||
221 | const accountName = comment.Account.getDisplayName() | ||
222 | const video = comment.Video | ||
223 | const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() | ||
224 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() | ||
225 | const commentHtml = toSafeHtml(comment.text) | ||
226 | |||
227 | const emailPayload: EmailPayload = { | ||
228 | template: 'video-comment-mention', | ||
229 | to, | ||
230 | subject: 'Mention on video ' + video.name, | ||
231 | locals: { | ||
232 | comment, | ||
233 | commentHtml, | ||
234 | video, | ||
235 | videoUrl, | ||
236 | accountName, | ||
237 | action: { | ||
238 | text: 'View comment', | ||
239 | url: commentUrl | ||
240 | } | ||
241 | } | ||
242 | } | ||
243 | |||
244 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
245 | } | ||
246 | |||
247 | addAbuseModeratorsNotification (to: string[], parameters: { | ||
248 | abuse: UserAbuse | ||
249 | abuseInstance: MAbuseFull | ||
250 | reporter: string | ||
251 | }) { | ||
252 | const { abuse, abuseInstance, reporter } = parameters | ||
253 | |||
254 | const action = { | ||
255 | text: 'View report #' + abuse.id, | ||
256 | url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id | ||
257 | } | ||
258 | |||
259 | let emailPayload: EmailPayload | ||
260 | |||
261 | if (abuseInstance.VideoAbuse) { | ||
262 | const video = abuseInstance.VideoAbuse.Video | ||
263 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | ||
264 | |||
265 | emailPayload = { | ||
266 | template: 'video-abuse-new', | ||
267 | to, | ||
268 | subject: `New video abuse report from ${reporter}`, | ||
269 | locals: { | ||
270 | videoUrl, | ||
271 | isLocal: video.remote === false, | ||
272 | videoCreatedAt: new Date(video.createdAt).toLocaleString(), | ||
273 | videoPublishedAt: new Date(video.publishedAt).toLocaleString(), | ||
274 | videoName: video.name, | ||
275 | reason: abuse.reason, | ||
276 | videoChannel: abuse.video.channel, | ||
277 | reporter, | ||
278 | action | ||
279 | } | ||
280 | } | ||
281 | } else if (abuseInstance.VideoCommentAbuse) { | ||
282 | const comment = abuseInstance.VideoCommentAbuse.VideoComment | ||
283 | const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() | ||
284 | |||
285 | emailPayload = { | ||
286 | template: 'video-comment-abuse-new', | ||
287 | to, | ||
288 | subject: `New comment abuse report from ${reporter}`, | ||
289 | locals: { | ||
290 | commentUrl, | ||
291 | videoName: comment.Video.name, | ||
292 | isLocal: comment.isOwned(), | ||
293 | commentCreatedAt: new Date(comment.createdAt).toLocaleString(), | ||
294 | reason: abuse.reason, | ||
295 | flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(), | ||
296 | reporter, | ||
297 | action | ||
298 | } | ||
299 | } | ||
300 | } else { | ||
301 | const account = abuseInstance.FlaggedAccount | ||
302 | const accountUrl = account.getClientUrl() | ||
303 | |||
304 | emailPayload = { | ||
305 | template: 'account-abuse-new', | ||
306 | to, | ||
307 | subject: `New account abuse report from ${reporter}`, | ||
308 | locals: { | ||
309 | accountUrl, | ||
310 | accountDisplayName: account.getDisplayName(), | ||
311 | isLocal: account.isOwned(), | ||
312 | reason: abuse.reason, | ||
313 | reporter, | ||
314 | action | ||
315 | } | ||
316 | } | ||
317 | } | ||
318 | |||
319 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
320 | } | ||
321 | |||
322 | addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) { | ||
323 | const text = abuse.state === AbuseState.ACCEPTED | ||
324 | ? 'Report #' + abuse.id + ' has been accepted' | ||
325 | : 'Report #' + abuse.id + ' has been rejected' | ||
326 | |||
327 | const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | ||
328 | |||
329 | const action = { | ||
330 | text, | ||
331 | url: abuseUrl | ||
332 | } | ||
333 | |||
334 | const emailPayload: EmailPayload = { | ||
335 | template: 'abuse-state-change', | ||
336 | to, | ||
337 | subject: text, | ||
338 | locals: { | ||
339 | action, | ||
340 | abuseId: abuse.id, | ||
341 | abuseUrl, | ||
342 | isAccepted: abuse.state === AbuseState.ACCEPTED | ||
343 | } | ||
344 | } | ||
345 | |||
346 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
347 | } | ||
348 | |||
349 | addAbuseNewMessageNotification ( | ||
350 | to: string[], | ||
351 | options: { | ||
352 | target: 'moderator' | 'reporter' | ||
353 | abuse: MAbuseFull | ||
354 | message: MAbuseMessage | ||
355 | accountMessage: MAccountDefault | ||
356 | }) { | ||
357 | const { abuse, target, message, accountMessage } = options | ||
358 | |||
359 | const text = 'New message on report #' + abuse.id | ||
360 | const abuseUrl = target === 'moderator' | ||
361 | ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id | ||
362 | : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | ||
363 | |||
364 | const action = { | ||
365 | text, | ||
366 | url: abuseUrl | ||
367 | } | ||
368 | |||
369 | const emailPayload: EmailPayload = { | ||
370 | template: 'abuse-new-message', | ||
371 | to, | ||
372 | subject: text, | ||
373 | locals: { | ||
374 | abuseId: abuse.id, | ||
375 | abuseUrl: action.url, | ||
376 | messageAccountName: accountMessage.getDisplayName(), | ||
377 | messageText: message.message, | ||
378 | action | ||
379 | } | ||
380 | } | ||
381 | |||
382 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
383 | } | ||
384 | |||
385 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | ||
386 | const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | ||
387 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | ||
388 | const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | ||
389 | |||
390 | const emailPayload: EmailPayload = { | ||
391 | template: 'video-auto-blacklist-new', | ||
392 | to, | ||
393 | subject: 'A new video is pending moderation', | ||
394 | locals: { | ||
395 | channel, | ||
396 | videoUrl, | ||
397 | videoName: videoBlacklist.Video.name, | ||
398 | action: { | ||
399 | text: 'Review autoblacklist', | ||
400 | url: videoAutoBlacklistUrl | ||
401 | } | ||
402 | } | ||
403 | } | ||
404 | |||
405 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
406 | } | ||
407 | |||
408 | addNewUserRegistrationNotification (to: string[], user: MUser) { | ||
409 | const emailPayload: EmailPayload = { | ||
410 | template: 'user-registered', | ||
411 | to, | ||
412 | subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${user.username}`, | ||
413 | locals: { | ||
414 | user | ||
415 | } | ||
416 | } | ||
417 | |||
418 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
419 | } | ||
420 | |||
421 | addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) { | ||
422 | const videoName = videoBlacklist.Video.name | ||
423 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | ||
424 | |||
425 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' | ||
426 | const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.INSTANCE.NAME} has been blacklisted${reasonString}.` | ||
427 | |||
428 | const emailPayload: EmailPayload = { | ||
429 | to, | ||
430 | subject: `Video ${videoName} blacklisted`, | ||
431 | text: blockedString, | ||
432 | locals: { | ||
433 | title: 'Your video was blacklisted' | ||
434 | } | ||
435 | } | ||
436 | |||
437 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
438 | } | ||
439 | |||
440 | addVideoUnblacklistNotification (to: string[], video: MVideo) { | ||
441 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | ||
442 | |||
443 | const emailPayload: EmailPayload = { | ||
444 | to, | ||
445 | subject: `Video ${video.name} unblacklisted`, | ||
446 | text: `Your video "${video.name}" (${videoUrl}) on ${CONFIG.INSTANCE.NAME} has been unblacklisted.`, | ||
447 | locals: { | ||
448 | title: 'Your video was unblacklisted' | ||
449 | } | ||
450 | } | ||
451 | |||
452 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
453 | } | ||
454 | |||
455 | addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { | ||
456 | const emailPayload: EmailPayload = { | ||
457 | to, | ||
458 | template: 'peertube-version-new', | ||
459 | subject: `A new PeerTube version is available: ${latestVersion}`, | ||
460 | locals: { | ||
461 | latestVersion | ||
462 | } | ||
463 | } | ||
464 | |||
465 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
466 | } | ||
467 | |||
468 | addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { | ||
469 | const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type | ||
470 | |||
471 | const emailPayload: EmailPayload = { | ||
472 | to, | ||
473 | template: 'plugin-version-new', | ||
474 | subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, | ||
475 | locals: { | ||
476 | pluginName: plugin.name, | ||
477 | latestVersion: plugin.latestVersion, | ||
478 | pluginUrl | ||
479 | } | ||
480 | } | ||
481 | |||
482 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
483 | } | ||
484 | |||
485 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { | 57 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { |
486 | const emailPayload: EmailPayload = { | 58 | const emailPayload: EmailPayload = { |
487 | template: 'password-reset', | 59 | template: 'password-reset', |
@@ -578,7 +150,11 @@ class Emailer { | |||
578 | subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX | 150 | subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX |
579 | }) | 151 | }) |
580 | 152 | ||
581 | for (const to of options.to) { | 153 | const toEmails = isArray(options.to) |
154 | ? options.to | ||
155 | : [ options.to ] | ||
156 | |||
157 | for (const to of toEmails) { | ||
582 | const baseOptions: SendEmailDefaultOptions = { | 158 | const baseOptions: SendEmailDefaultOptions = { |
583 | template: 'common', | 159 | template: 'common', |
584 | message: { | 160 | message: { |