diff options
Diffstat (limited to 'server/lib/emailer.ts')
-rw-r--r-- | server/lib/emailer.ts | 261 |
1 files changed, 217 insertions, 44 deletions
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 9327792fb..f384a254e 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | 1 | import { createTransport, Transporter } from 'nodemailer' |
2 | import { UserRight } from '../../shared/models/users' | ||
3 | import { isTestInstance } from '../helpers/core-utils' | 2 | import { isTestInstance } from '../helpers/core-utils' |
4 | import { bunyanLogger, logger } from '../helpers/logger' | 3 | import { bunyanLogger, logger } from '../helpers/logger' |
5 | import { CONFIG } from '../initializers' | 4 | import { CONFIG } from '../initializers' |
@@ -8,6 +7,11 @@ import { VideoModel } from '../models/video/video' | |||
8 | import { JobQueue } from './job-queue' | 7 | import { JobQueue } from './job-queue' |
9 | import { EmailPayload } from './job-queue/handlers/email' | 8 | import { EmailPayload } from './job-queue/handlers/email' |
10 | import { readFileSync } from 'fs-extra' | 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' | ||
11 | 15 | ||
12 | class Emailer { | 16 | class Emailer { |
13 | 17 | ||
@@ -22,7 +26,7 @@ class Emailer { | |||
22 | if (this.initialized === true) return | 26 | if (this.initialized === true) return |
23 | this.initialized = true | 27 | this.initialized = true |
24 | 28 | ||
25 | if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) { | 29 | if (Emailer.isEnabled()) { |
26 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) | 30 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) |
27 | 31 | ||
28 | let tls | 32 | let tls |
@@ -57,6 +61,10 @@ class Emailer { | |||
57 | } | 61 | } |
58 | } | 62 | } |
59 | 63 | ||
64 | static isEnabled () { | ||
65 | return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT | ||
66 | } | ||
67 | |||
60 | async checkConnectionOrDie () { | 68 | async checkConnectionOrDie () { |
61 | if (!this.transporter) return | 69 | if (!this.transporter) return |
62 | 70 | ||
@@ -72,50 +80,158 @@ class Emailer { | |||
72 | } | 80 | } |
73 | } | 81 | } |
74 | 82 | ||
75 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | 83 | addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) { |
84 | const channelName = video.VideoChannel.getDisplayName() | ||
85 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() | ||
86 | |||
76 | const text = `Hi dear user,\n\n` + | 87 | const text = `Hi dear user,\n\n` + |
77 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | 88 | `Your subscription ${channelName} just published a new video: ${video.name}` + |
78 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | 89 | `\n\n` + |
79 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 90 | `You can view it on ${videoUrl} ` + |
91 | `\n\n` + | ||
80 | `Cheers,\n` + | 92 | `Cheers,\n` + |
81 | `PeerTube.` | 93 | `PeerTube.` |
82 | 94 | ||
83 | const emailPayload: EmailPayload = { | 95 | const emailPayload: EmailPayload = { |
84 | to: [ to ], | 96 | to, |
85 | subject: 'Reset your PeerTube password', | 97 | subject: channelName + ' just published a new video', |
86 | text | 98 | text |
87 | } | 99 | } |
88 | 100 | ||
89 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 101 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
90 | } | 102 | } |
91 | 103 | ||
92 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | 104 | addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { |
93 | const text = `Welcome to PeerTube,\n\n` + | 105 | const followerName = actorFollow.ActorFollower.Account.getDisplayName() |
94 | `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + | 106 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() |
95 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | 107 | |
96 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 108 | const text = `Hi dear user,\n\n` + |
109 | `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + | ||
110 | `\n\n` + | ||
97 | `Cheers,\n` + | 111 | `Cheers,\n` + |
98 | `PeerTube.` | 112 | `PeerTube.` |
99 | 113 | ||
100 | const emailPayload: EmailPayload = { | 114 | const emailPayload: EmailPayload = { |
101 | to: [ to ], | 115 | to, |
102 | subject: 'Verify your PeerTube email', | 116 | subject: 'New follower on your channel ' + followingName, |
117 | text | ||
118 | } | ||
119 | |||
120 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
121 | } | ||
122 | |||
123 | myVideoPublishedNotification (to: string[], video: VideoModel) { | ||
124 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() | ||
125 | |||
126 | const text = `Hi dear user,\n\n` + | ||
127 | `Your video ${video.name} has been published.` + | ||
128 | `\n\n` + | ||
129 | `You can view it on ${videoUrl} ` + | ||
130 | `\n\n` + | ||
131 | `Cheers,\n` + | ||
132 | `PeerTube.` | ||
133 | |||
134 | const emailPayload: EmailPayload = { | ||
135 | to, | ||
136 | subject: `Your video ${video.name} is published`, | ||
137 | text | ||
138 | } | ||
139 | |||
140 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
141 | } | ||
142 | |||
143 | myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) { | ||
144 | const videoUrl = CONFIG.WEBSERVER.URL + videoImport.Video.getWatchStaticPath() | ||
145 | |||
146 | const text = `Hi dear user,\n\n` + | ||
147 | `Your video import ${videoImport.getTargetIdentifier()} is finished.` + | ||
148 | `\n\n` + | ||
149 | `You can view the imported video on ${videoUrl} ` + | ||
150 | `\n\n` + | ||
151 | `Cheers,\n` + | ||
152 | `PeerTube.` | ||
153 | |||
154 | const emailPayload: EmailPayload = { | ||
155 | to, | ||
156 | subject: `Your video import ${videoImport.getTargetIdentifier()} is finished`, | ||
103 | text | 157 | text |
104 | } | 158 | } |
105 | 159 | ||
106 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 160 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
107 | } | 161 | } |
108 | 162 | ||
109 | async addVideoAbuseReportJob (videoId: number) { | 163 | myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { |
110 | const video = await VideoModel.load(videoId) | 164 | const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports' |
111 | if (!video) throw new Error('Unknown Video id during Abuse report.') | 165 | |
166 | const text = `Hi dear user,\n\n` + | ||
167 | `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + | ||
168 | `\n\n` + | ||
169 | `See your videos import dashboard for more information: ${importUrl}` + | ||
170 | `\n\n` + | ||
171 | `Cheers,\n` + | ||
172 | `PeerTube.` | ||
173 | |||
174 | const emailPayload: EmailPayload = { | ||
175 | to, | ||
176 | subject: `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, | ||
177 | text | ||
178 | } | ||
179 | |||
180 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
181 | } | ||
182 | |||
183 | addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { | ||
184 | const accountName = comment.Account.getDisplayName() | ||
185 | const video = comment.Video | ||
186 | const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() | ||
187 | |||
188 | const text = `Hi dear user,\n\n` + | ||
189 | `A new comment has been posted by ${accountName} on your video ${video.name}` + | ||
190 | `\n\n` + | ||
191 | `You can view it on ${commentUrl} ` + | ||
192 | `\n\n` + | ||
193 | `Cheers,\n` + | ||
194 | `PeerTube.` | ||
195 | |||
196 | const emailPayload: EmailPayload = { | ||
197 | to, | ||
198 | subject: 'New comment on your video ' + video.name, | ||
199 | text | ||
200 | } | ||
201 | |||
202 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
203 | } | ||
204 | |||
205 | addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { | ||
206 | const accountName = comment.Account.getDisplayName() | ||
207 | const video = comment.Video | ||
208 | const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() | ||
209 | |||
210 | const text = `Hi dear user,\n\n` + | ||
211 | `${accountName} mentioned you on video ${video.name}` + | ||
212 | `\n\n` + | ||
213 | `You can view the comment on ${commentUrl} ` + | ||
214 | `\n\n` + | ||
215 | `Cheers,\n` + | ||
216 | `PeerTube.` | ||
217 | |||
218 | const emailPayload: EmailPayload = { | ||
219 | to, | ||
220 | subject: 'Mention on video ' + video.name, | ||
221 | text | ||
222 | } | ||
223 | |||
224 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
225 | } | ||
226 | |||
227 | addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { | ||
228 | const videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() | ||
112 | 229 | ||
113 | const text = `Hi,\n\n` + | 230 | const text = `Hi,\n\n` + |
114 | `Your instance received an abuse for the following video ${video.url}\n\n` + | 231 | `${CONFIG.WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + |
115 | `Cheers,\n` + | 232 | `Cheers,\n` + |
116 | `PeerTube.` | 233 | `PeerTube.` |
117 | 234 | ||
118 | const to = await UserModel.listEmailsWithRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
119 | const emailPayload: EmailPayload = { | 235 | const emailPayload: EmailPayload = { |
120 | to, | 236 | to, |
121 | subject: '[PeerTube] Received a video abuse', | 237 | subject: '[PeerTube] Received a video abuse', |
@@ -125,16 +241,27 @@ class Emailer { | |||
125 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 241 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
126 | } | 242 | } |
127 | 243 | ||
128 | async addVideoBlacklistReportJob (videoId: number, reason?: string) { | 244 | addNewUserRegistrationNotification (to: string[], user: UserModel) { |
129 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 245 | const text = `Hi,\n\n` + |
130 | if (!video) throw new Error('Unknown Video id during Blacklist report.') | 246 | `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + |
131 | // It's not our user | 247 | `Cheers,\n` + |
132 | if (video.remote === true) return | 248 | `PeerTube.` |
133 | 249 | ||
134 | const user = await UserModel.loadById(video.VideoChannel.Account.userId) | 250 | const emailPayload: EmailPayload = { |
251 | to, | ||
252 | subject: '[PeerTube] New user registration on ' + CONFIG.WEBSERVER.HOST, | ||
253 | text | ||
254 | } | ||
135 | 255 | ||
136 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 256 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
137 | const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.` | 257 | } |
258 | |||
259 | addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { | ||
260 | const videoName = videoBlacklist.Video.name | ||
261 | const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | ||
262 | |||
263 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' | ||
264 | const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.` | ||
138 | 265 | ||
139 | const text = 'Hi,\n\n' + | 266 | const text = 'Hi,\n\n' + |
140 | blockedString + | 267 | blockedString + |
@@ -142,33 +269,26 @@ class Emailer { | |||
142 | 'Cheers,\n' + | 269 | 'Cheers,\n' + |
143 | `PeerTube.` | 270 | `PeerTube.` |
144 | 271 | ||
145 | const to = user.email | ||
146 | const emailPayload: EmailPayload = { | 272 | const emailPayload: EmailPayload = { |
147 | to: [ to ], | 273 | to, |
148 | subject: `[PeerTube] Video ${video.name} blacklisted`, | 274 | subject: `[PeerTube] Video ${videoName} blacklisted`, |
149 | text | 275 | text |
150 | } | 276 | } |
151 | 277 | ||
152 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 278 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
153 | } | 279 | } |
154 | 280 | ||
155 | async addVideoUnblacklistReportJob (videoId: number) { | 281 | addVideoUnblacklistNotification (to: string[], video: VideoModel) { |
156 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 282 | const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() |
157 | if (!video) throw new Error('Unknown Video id during Blacklist report.') | ||
158 | // It's not our user | ||
159 | if (video.remote === true) return | ||
160 | |||
161 | const user = await UserModel.loadById(video.VideoChannel.Account.userId) | ||
162 | 283 | ||
163 | const text = 'Hi,\n\n' + | 284 | const text = 'Hi,\n\n' + |
164 | `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + | 285 | `Your video ${video.name} (${videoUrl}) on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + |
165 | '\n\n' + | 286 | '\n\n' + |
166 | 'Cheers,\n' + | 287 | 'Cheers,\n' + |
167 | `PeerTube.` | 288 | `PeerTube.` |
168 | 289 | ||
169 | const to = user.email | ||
170 | const emailPayload: EmailPayload = { | 290 | const emailPayload: EmailPayload = { |
171 | to: [ to ], | 291 | to, |
172 | subject: `[PeerTube] Video ${video.name} unblacklisted`, | 292 | subject: `[PeerTube] Video ${video.name} unblacklisted`, |
173 | text | 293 | text |
174 | } | 294 | } |
@@ -176,6 +296,40 @@ class Emailer { | |||
176 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
177 | } | 297 | } |
178 | 298 | ||
299 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | ||
300 | const text = `Hi dear user,\n\n` + | ||
301 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | ||
302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | ||
303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
304 | `Cheers,\n` + | ||
305 | `PeerTube.` | ||
306 | |||
307 | const emailPayload: EmailPayload = { | ||
308 | to: [ to ], | ||
309 | subject: 'Reset your PeerTube password', | ||
310 | text | ||
311 | } | ||
312 | |||
313 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
314 | } | ||
315 | |||
316 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | ||
317 | const text = `Welcome to PeerTube,\n\n` + | ||
318 | `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + | ||
319 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | ||
320 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
321 | `Cheers,\n` + | ||
322 | `PeerTube.` | ||
323 | |||
324 | const emailPayload: EmailPayload = { | ||
325 | to: [ to ], | ||
326 | subject: 'Verify your PeerTube email', | ||
327 | text | ||
328 | } | ||
329 | |||
330 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
331 | } | ||
332 | |||
179 | addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { | 333 | addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { |
180 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 334 | const reasonString = reason ? ` for the following reason: ${reason}` : '' |
181 | const blockedWord = blocked ? 'blocked' : 'unblocked' | 335 | const blockedWord = blocked ? 'blocked' : 'unblocked' |
@@ -197,13 +351,32 @@ class Emailer { | |||
197 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 351 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
198 | } | 352 | } |
199 | 353 | ||
200 | sendMail (to: string[], subject: string, text: string) { | 354 | addContactFormJob (fromEmail: string, fromName: string, body: string) { |
201 | if (!this.transporter) { | 355 | const text = 'Hello dear admin,\n\n' + |
356 | fromName + ' sent you a message' + | ||
357 | '\n\n---------------------------------------\n\n' + | ||
358 | body + | ||
359 | '\n\n---------------------------------------\n\n' + | ||
360 | 'Cheers,\n' + | ||
361 | 'PeerTube.' | ||
362 | |||
363 | const emailPayload: EmailPayload = { | ||
364 | from: fromEmail, | ||
365 | to: [ CONFIG.ADMIN.EMAIL ], | ||
366 | subject: '[PeerTube] Contact form submitted', | ||
367 | text | ||
368 | } | ||
369 | |||
370 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
371 | } | ||
372 | |||
373 | sendMail (to: string[], subject: string, text: string, from?: string) { | ||
374 | if (!Emailer.isEnabled()) { | ||
202 | throw new Error('Cannot send mail because SMTP is not configured.') | 375 | throw new Error('Cannot send mail because SMTP is not configured.') |
203 | } | 376 | } |
204 | 377 | ||
205 | return this.transporter.sendMail({ | 378 | return this.transporter.sendMail({ |
206 | from: CONFIG.SMTP.FROM_ADDRESS, | 379 | from: from || CONFIG.SMTP.FROM_ADDRESS, |
207 | to: to.join(','), | 380 | to: to.join(','), |
208 | subject, | 381 | subject, |
209 | text | 382 | text |