diff options
Diffstat (limited to 'server/lib/emailer.ts')
-rw-r--r-- | server/lib/emailer.ts | 477 |
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 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | 1 | import { createTransport, Transporter } from 'nodemailer' |
2 | import { isTestInstance } from '../helpers/core-utils' | 2 | import { isTestInstance, root } from '../helpers/core-utils' |
3 | import { bunyanLogger, logger } from '../helpers/logger' | 3 | import { bunyanLogger, logger } from '../helpers/logger' |
4 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
5 | import { JobQueue } from './job-queue' | 5 | import { JobQueue } from './job-queue' |
6 | import { EmailPayload } from './job-queue/handlers/email' | ||
7 | import { readFileSync } from 'fs-extra' | 6 | import { readFileSync } from 'fs-extra' |
8 | import { WEBSERVER } from '../initializers/constants' | 7 | import { WEBSERVER } from '../initializers/constants' |
9 | import { | 8 | import { |
@@ -16,15 +15,13 @@ import { | |||
16 | } from '../typings/models/video' | 15 | } from '../typings/models/video' |
17 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' | 16 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' |
18 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' | 17 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' |
19 | 18 | import { EmailPayload } from '@shared/models' | |
20 | type SendEmailOptions = { | 19 | import { join } from 'path' |
21 | to: string[] | 20 | import { VideoAbuse } from '../../shared/models/videos' |
22 | subject: string | 21 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' |
23 | text: string | 22 | import { merge } from 'lodash' |
24 | 23 | import { VideoChannelModel } from '@server/models/video/video-channel' | |
25 | fromDisplayName?: string | 24 | const Email = require('email-templates') |
26 | replyTo?: string | ||
27 | } | ||
28 | 25 | ||
29 | class Emailer { | 26 | class 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 | ||
476 | export { | 518 | export { |
477 | Emailer, | 519 | Emailer |
478 | SendEmailOptions | ||
479 | } | 520 | } |