aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-05-05 20:22:22 +0200
committerRigel Kent <par@rigelk.eu>2020-05-08 15:31:51 +0200
commitdf4c603dea022146476812cbbc2b9f8f1e5e4870 (patch)
treec0d27576fb6711b4b64d2186e8dca3f04b9b1dfe /server
parent91b8e675e26dd65e1ebb23706cb16b3a3f8bcf73 (diff)
downloadPeerTube-df4c603dea022146476812cbbc2b9f8f1e5e4870.tar.gz
PeerTube-df4c603dea022146476812cbbc2b9f8f1e5e4870.tar.zst
PeerTube-df4c603dea022146476812cbbc2b9f8f1e5e4870.zip
Switch emails to pug templates and provide richer html/text-only versions
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/abuse.ts20
-rw-r--r--server/lib/activitypub/process/process-flag.ts17
-rw-r--r--server/lib/emailer.ts392
-rw-r--r--server/lib/emails/common/base.pug267
-rw-r--r--server/lib/emails/common/greetings.pug11
-rw-r--r--server/lib/emails/common/html.pug4
-rw-r--r--server/lib/emails/common/mixins.pug3
-rw-r--r--server/lib/emails/contact-form/html.pug9
-rw-r--r--server/lib/emails/follower-on-channel/html.pug9
-rw-r--r--server/lib/emails/password-create/html.pug10
-rw-r--r--server/lib/emails/password-reset/html.pug12
-rw-r--r--server/lib/emails/user-registered/html.pug10
-rw-r--r--server/lib/emails/verify-email/html.pug14
-rw-r--r--server/lib/emails/video-abuse-new/html.pug18
-rw-r--r--server/lib/emails/video-auto-blacklist-new/html.pug17
-rw-r--r--server/lib/emails/video-comment-mention/html.pug11
-rw-r--r--server/lib/emails/video-comment-new/html.pug11
-rw-r--r--server/lib/notifier.ts22
-rw-r--r--server/tests/api/server/contact-form.ts2
19 files changed, 652 insertions, 207 deletions
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index bce50aefb..ec28fce67 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared' 2import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers/database' 5import { sequelizeTypescript } from '../../../initializers/database'
@@ -24,6 +24,7 @@ import { Notifier } from '../../../lib/notifier'
24import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' 24import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
25import { MVideoAbuseAccountVideo } from '../../../typings/models/video' 25import { MVideoAbuseAccountVideo } from '../../../typings/models/video'
26import { getServerActor } from '@server/models/application/application' 26import { getServerActor } from '@server/models/application/application'
27import { MAccountDefault } from '@server/typings/models'
27 28
28const auditLogger = auditLoggerFactory('abuse') 29const auditLogger = auditLoggerFactory('abuse')
29const abuseVideoRouter = express.Router() 30const abuseVideoRouter = express.Router()
@@ -117,9 +118,11 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
117async function reportVideoAbuse (req: express.Request, res: express.Response) { 118async function reportVideoAbuse (req: express.Request, res: express.Response) {
118 const videoInstance = res.locals.videoAll 119 const videoInstance = res.locals.videoAll
119 const body: VideoAbuseCreate = req.body 120 const body: VideoAbuseCreate = req.body
121 let reporterAccount: MAccountDefault
122 let videoAbuseJSON: VideoAbuse
120 123
121 const videoAbuse = await sequelizeTypescript.transaction(async t => { 124 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
122 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 125 reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
123 126
124 const abuseToCreate = { 127 const abuseToCreate = {
125 reporterAccountId: reporterAccount.id, 128 reporterAccountId: reporterAccount.id,
@@ -137,14 +140,19 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
137 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t) 140 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
138 } 141 }
139 142
140 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) 143 videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
144 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))
141 145
142 return videoAbuseInstance 146 return videoAbuseInstance
143 }) 147 })
144 148
145 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 149 Notifier.Instance.notifyOnNewVideoAbuse({
150 videoAbuse: videoAbuseJSON,
151 videoAbuseInstance,
152 reporter: reporterAccount.Actor.getIdentifier()
153 })
146 154
147 logger.info('Abuse report for video %s created.', videoInstance.name) 155 logger.info('Abuse report for video %s created.', videoInstance.name)
148 156
149 return res.json({ videoAbuse: videoAbuse.toFormattedJSON() }).end() 157 return res.json({ videoAbuseJSON }).end()
150} 158}
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 9a488a473..7337f337c 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -8,7 +8,8 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub' 9import { getAPId } from '../../../helpers/activitypub'
10import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 10import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
11import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models' 11import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models'
12import { AccountModel } from '@server/models/account/account'
12 13
13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 14async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
14 const { activity, byActor } = options 15 const { activity, byActor } = options
@@ -36,8 +37,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
36 logger.debug('Reporting remote abuse for video %s.', getAPId(object)) 37 logger.debug('Reporting remote abuse for video %s.', getAPId(object))
37 38
38 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) 39 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
40 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
39 41
40 const videoAbuse = await sequelizeTypescript.transaction(async t => { 42 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
41 const videoAbuseData = { 43 const videoAbuseData = {
42 reporterAccountId: account.id, 44 reporterAccountId: account.id,
43 reason: flag.content, 45 reason: flag.content,
@@ -45,15 +47,22 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
45 state: VideoAbuseState.PENDING 47 state: VideoAbuseState.PENDING
46 } 48 }
47 49
48 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo 50 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
49 videoAbuseInstance.Video = video 51 videoAbuseInstance.Video = video
52 videoAbuseInstance.Account = reporterAccount
50 53
51 logger.info('Remote abuse for video uuid %s created', flag.object) 54 logger.info('Remote abuse for video uuid %s created', flag.object)
52 55
53 return videoAbuseInstance 56 return videoAbuseInstance
54 }) 57 })
55 58
56 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 59 const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
60
61 Notifier.Instance.notifyOnNewVideoAbuse({
62 videoAbuse: videoAbuseJSON,
63 videoAbuseInstance,
64 reporter: reporterAccount.Actor.getIdentifier()
65 })
57 } catch (err) { 66 } catch (err) {
58 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) 67 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
59 } 68 }
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 45d57fd28..935c9e882 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,5 +1,5 @@
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, isEmailEnabled } from '../initializers/config' 4import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue' 5import { JobQueue } from './job-queue'
@@ -16,6 +16,12 @@ import {
16import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' 16import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
17import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' 17import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
18import { EmailPayload } from '@shared/models' 18import { EmailPayload } from '@shared/models'
19import { join } from 'path'
20import { VideoAbuse } from '../../shared/models/videos'
21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
22import { merge } from 'lodash'
23import { VideoChannelModel } from '@server/models/video/video-channel'
24const Email = require('email-templates')
19 25
20class Emailer { 26class Emailer {
21 27
@@ -105,37 +111,36 @@ class Emailer {
105 const channelName = video.VideoChannel.getDisplayName() 111 const channelName = video.VideoChannel.getDisplayName()
106 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 112 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
107 113
108 const text = 'Hi dear user,\n\n' +
109 `Your subscription ${channelName} just published a new video: ${video.name}` +
110 '\n\n' +
111 `You can view it on ${videoUrl} ` +
112 '\n\n' +
113 'Cheers,\n' +
114 `${CONFIG.EMAIL.BODY.SIGNATURE}`
115
116 const emailPayload: EmailPayload = { 114 const emailPayload: EmailPayload = {
117 to, 115 to,
118 subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video', 116 subject: channelName + ' just published a new video',
119 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 }
120 } 125 }
121 126
122 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 127 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
123 } 128 }
124 129
125 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { 130 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
126 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
127 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 131 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
128 132
129 const text = 'Hi dear user,\n\n' +
130 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
131 '\n\n' +
132 'Cheers,\n' +
133 `${CONFIG.EMAIL.BODY.SIGNATURE}`
134
135 const emailPayload: EmailPayload = { 133 const emailPayload: EmailPayload = {
134 template: 'follower-on-channel',
136 to, 135 to,
137 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName, 136 subject: `New follower on your channel ${followingName}`,
138 text 137 locals: {
138 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
139 followerUrl: actorFollow.ActorFollower.url,
140 followingName,
141 followingUrl: actorFollow.ActorFollowing.url,
142 followType
143 }
139 } 144 }
140 145
141 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 146 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -144,32 +149,28 @@ class Emailer {
144 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { 149 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
145 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 150 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
146 151
147 const text = 'Hi dear admin,\n\n' +
148 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
149 '\n\n' +
150 'Cheers,\n' +
151 `${CONFIG.EMAIL.BODY.SIGNATURE}`
152
153 const emailPayload: EmailPayload = { 152 const emailPayload: EmailPayload = {
154 to, 153 to,
155 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower', 154 subject: 'New instance follower',
156 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 }
157 } 163 }
158 164
159 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 165 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
160 } 166 }
161 167
162 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { 168 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
163 const text = 'Hi dear admin,\n\n' + 169 const instanceUrl = actorFollow.ActorFollowing.url
164 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
165 '\n\n' +
166 'Cheers,\n' +
167 `${CONFIG.EMAIL.BODY.SIGNATURE}`
168
169 const emailPayload: EmailPayload = { 170 const emailPayload: EmailPayload = {
170 to, 171 to,
171 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', 172 subject: 'Auto instance following',
172 text 173 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
173 } 174 }
174 175
175 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 176 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -178,18 +179,17 @@ class Emailer {
178 myVideoPublishedNotification (to: string[], video: MVideo) { 179 myVideoPublishedNotification (to: string[], video: MVideo) {
179 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 180 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
180 181
181 const text = 'Hi dear user,\n\n' +
182 `Your video ${video.name} has been published.` +
183 '\n\n' +
184 `You can view it on ${videoUrl} ` +
185 '\n\n' +
186 'Cheers,\n' +
187 `${CONFIG.EMAIL.BODY.SIGNATURE}`
188
189 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
190 to, 183 to,
191 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`, 184 subject: `Your video ${video.name} has been published`,
192 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 }
193 } 193 }
194 194
195 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 195 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -198,18 +198,17 @@ class Emailer {
198 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { 198 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
199 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 199 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
200 200
201 const text = 'Hi dear user,\n\n' +
202 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
203 '\n\n' +
204 `You can view the imported video on ${videoUrl} ` +
205 '\n\n' +
206 'Cheers,\n' +
207 `${CONFIG.EMAIL.BODY.SIGNATURE}`
208
209 const emailPayload: EmailPayload = { 201 const emailPayload: EmailPayload = {
210 to, 202 to,
211 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`, 203 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
212 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 }
213 } 212 }
214 213
215 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 214 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -218,40 +217,47 @@ class Emailer {
218 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { 217 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
219 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 218 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
220 219
221 const text = 'Hi dear user,\n\n' + 220 const text =
222 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 221 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
223 '\n\n' +
224 `See your videos import dashboard for more information: ${importUrl}` +
225 '\n\n' + 222 '\n\n' +
226 'Cheers,\n' + 223 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
227 `${CONFIG.EMAIL.BODY.SIGNATURE}`
228 224
229 const emailPayload: EmailPayload = { 225 const emailPayload: EmailPayload = {
230 to, 226 to,
231 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, 227 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
232 text 228 text,
229 locals: {
230 title: 'Import failed',
231 action: {
232 text: 'Review imports',
233 url: importUrl
234 }
235 }
233 } 236 }
234 237
235 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 238 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
236 } 239 }
237 240
238 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { 241 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
239 const accountName = comment.Account.getDisplayName()
240 const video = comment.Video 242 const video = comment.Video
243 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
241 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 244 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
242 245
243 const text = 'Hi dear user,\n\n' +
244 `A new comment has been posted by ${accountName} on your video ${video.name}` +
245 '\n\n' +
246 `You can view it on ${commentUrl} ` +
247 '\n\n' +
248 'Cheers,\n' +
249 `${CONFIG.EMAIL.BODY.SIGNATURE}`
250
251 const emailPayload: EmailPayload = { 246 const emailPayload: EmailPayload = {
247 template: 'video-comment-new',
252 to, 248 to,
253 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name, 249 subject: 'New comment on your video ' + video.name,
254 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 }
255 } 261 }
256 262
257 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 263 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -260,75 +266,88 @@ class Emailer {
260 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { 266 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
261 const accountName = comment.Account.getDisplayName() 267 const accountName = comment.Account.getDisplayName()
262 const video = comment.Video 268 const video = comment.Video
269 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
263 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 270 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
264 271
265 const text = 'Hi dear user,\n\n' +
266 `${accountName} mentioned you on video ${video.name}` +
267 '\n\n' +
268 `You can view the comment on ${commentUrl} ` +
269 '\n\n' +
270 'Cheers,\n' +
271 `${CONFIG.EMAIL.BODY.SIGNATURE}`
272
273 const emailPayload: EmailPayload = { 272 const emailPayload: EmailPayload = {
273 template: 'video-comment-mention',
274 to, 274 to,
275 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name, 275 subject: 'Mention on video ' + video.name,
276 text 276 locals: {
277 comment,
278 video,
279 videoUrl,
280 accountName,
281 action: {
282 text: 'View comment',
283 url: commentUrl
284 }
285 }
277 } 286 }
278 287
279 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
280 } 289 }
281 290
282 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { 291 addVideoAbuseModeratorsNotification (to: string[], parameters: {
283 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 292 videoAbuse: VideoAbuse
284 293 videoAbuseInstance: MVideoAbuseVideo
285 const text = 'Hi,\n\n' + 294 reporter: string
286 `${WEBSERVER.HOST} received an abuse for the following video: ${videoUrl}\n\n` + 295 }) {
287 'Cheers,\n' + 296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
288 `${CONFIG.EMAIL.BODY.SIGNATURE}` 297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
289 298
290 const emailPayload: EmailPayload = { 299 const emailPayload: EmailPayload = {
300 template: 'video-abuse-new',
291 to, 301 to,
292 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse', 302 subject: `New video abuse report from ${parameters.reporter}`,
293 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 }
294 } 315 }
295 316
296 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 317 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
297 } 318 }
298 319
299 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 320 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
300 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'
301 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 322 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
302 323 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
303 const text = 'Hi,\n\n' +
304 'A recently added video was auto-blacklisted and requires moderator review before publishing.' +
305 '\n\n' +
306 `You can view it and take appropriate action on ${videoUrl}` +
307 '\n\n' +
308 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
309 '\n\n' +
310 'Cheers,\n' +
311 `${CONFIG.EMAIL.BODY.SIGNATURE}`
312 324
313 const emailPayload: EmailPayload = { 325 const emailPayload: EmailPayload = {
326 template: 'video-auto-blacklist-new',
314 to, 327 to,
315 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review', 328 subject: 'A new video is pending moderation',
316 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 }
317 } 338 }
318 339
319 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 340 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
320 } 341 }
321 342
322 addNewUserRegistrationNotification (to: string[], user: MUser) { 343 addNewUserRegistrationNotification (to: string[], user: MUser) {
323 const text = 'Hi,\n\n' +
324 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
325 'Cheers,\n' +
326 `${CONFIG.EMAIL.BODY.SIGNATURE}`
327
328 const emailPayload: EmailPayload = { 344 const emailPayload: EmailPayload = {
345 template: 'user-registered',
329 to, 346 to,
330 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST, 347 subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
331 text 348 locals: {
349 user
350 }
332 } 351 }
333 352
334 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 353 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -341,16 +360,13 @@ class Emailer {
341 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' 360 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
342 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}.`
343 362
344 const text = 'Hi,\n\n' +
345 blockedString +
346 '\n\n' +
347 'Cheers,\n' +
348 `${CONFIG.EMAIL.BODY.SIGNATURE}`
349
350 const emailPayload: EmailPayload = { 363 const emailPayload: EmailPayload = {
351 to, 364 to,
352 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`, 365 subject: `Video ${videoName} blacklisted`,
353 text 366 text: blockedString,
367 locals: {
368 title: 'Your video was blacklisted'
369 }
354 } 370 }
355 371
356 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 372 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -359,66 +375,53 @@ class Emailer {
359 addVideoUnblacklistNotification (to: string[], video: MVideo) { 375 addVideoUnblacklistNotification (to: string[], video: MVideo) {
360 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 376 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
361 377
362 const text = 'Hi,\n\n' +
363 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
364 '\n\n' +
365 'Cheers,\n' +
366 `${CONFIG.EMAIL.BODY.SIGNATURE}`
367
368 const emailPayload: EmailPayload = { 378 const emailPayload: EmailPayload = {
369 to, 379 to,
370 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`, 380 subject: `Video ${video.name} unblacklisted`,
371 text 381 text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
382 locals: {
383 title: 'Your video was unblacklisted'
384 }
372 } 385 }
373 386
374 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 387 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
375 } 388 }
376 389
377 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { 390 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
378 const text = 'Hi dear user,\n\n' +
379 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
380 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` +
381 'If you are not the person who initiated this request, please ignore this email.\n\n' +
382 'Cheers,\n' +
383 `${CONFIG.EMAIL.BODY.SIGNATURE}`
384
385 const emailPayload: EmailPayload = { 391 const emailPayload: EmailPayload = {
392 template: 'password-reset',
386 to: [ to ], 393 to: [ to ],
387 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password', 394 subject: 'Reset your account password',
388 text 395 locals: {
396 resetPasswordUrl
397 }
389 } 398 }
390 399
391 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
392 } 401 }
393 402
394 addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) { 403 addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
395 const text = 'Hi,\n\n' +
396 `Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` +
397 `Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` +
398 'Cheers,\n' +
399 `${CONFIG.EMAIL.BODY.SIGNATURE}`
400
401 const emailPayload: EmailPayload = { 404 const emailPayload: EmailPayload = {
405 template: 'password-create',
402 to: [ to ], 406 to: [ to ],
403 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password', 407 subject: 'Create your account password',
404 text 408 locals: {
409 username,
410 createPasswordUrl
411 }
405 } 412 }
406 413
407 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 414 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
408 } 415 }
409 416
410 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 417 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
411 const text = 'Welcome to PeerTube,\n\n' +
412 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
413 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
414 'If you are not the person who initiated this request, please ignore this email.\n\n' +
415 'Cheers,\n' +
416 `${CONFIG.EMAIL.BODY.SIGNATURE}`
417
418 const emailPayload: EmailPayload = { 418 const emailPayload: EmailPayload = {
419 template: 'verify-email',
419 to: [ to ], 420 to: [ to ],
420 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email', 421 subject: `Verify your email on ${WEBSERVER.HOST}`,
421 text 422 locals: {
423 verifyEmailUrl
424 }
422 } 425 }
423 426
424 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 427 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -427,39 +430,28 @@ class Emailer {
427 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { 430 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
428 const reasonString = reason ? ` for the following reason: ${reason}` : '' 431 const reasonString = reason ? ` for the following reason: ${reason}` : ''
429 const blockedWord = blocked ? 'blocked' : 'unblocked' 432 const blockedWord = blocked ? 'blocked' : 'unblocked'
430 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
431
432 const text = 'Hi,\n\n' +
433 blockedString +
434 '\n\n' +
435 'Cheers,\n' +
436 `${CONFIG.EMAIL.BODY.SIGNATURE}`
437 433
438 const to = user.email 434 const to = user.email
439 const emailPayload: EmailPayload = { 435 const emailPayload: EmailPayload = {
440 to: [ to ], 436 to: [ to ],
441 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord, 437 subject: 'Account ' + blockedWord,
442 text 438 text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
443 } 439 }
444 440
445 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
446 } 442 }
447 443
448 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { 444 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
449 const text = 'Hello dear admin,\n\n' +
450 fromName + ' sent you a message' +
451 '\n\n---------------------------------------\n\n' +
452 body +
453 '\n\n---------------------------------------\n\n' +
454 'Cheers,\n' +
455 'PeerTube.'
456
457 const emailPayload: EmailPayload = { 445 const emailPayload: EmailPayload = {
458 fromDisplayName: fromEmail, 446 template: 'contact-form',
459 replyTo: fromEmail,
460 to: [ CONFIG.ADMIN.EMAIL ], 447 to: [ CONFIG.ADMIN.EMAIL ],
461 subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject, 448 replyTo: `"${fromName}" <${fromEmail}>`,
462 text 449 subject: `(contact form) ${subject}`,
450 locals: {
451 fromName,
452 fromEmail,
453 body
454 }
463 } 455 }
464 456
465 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 457 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -470,18 +462,44 @@ class Emailer {
470 throw new Error('Cannot send mail because SMTP is not configured.') 462 throw new Error('Cannot send mail because SMTP is not configured.')
471 } 463 }
472 464
473 const fromDisplayName = options.fromDisplayName 465 const fromDisplayName = options.from
474 ? options.fromDisplayName 466 ? options.from
475 : WEBSERVER.HOST 467 : WEBSERVER.HOST
476 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
477 for (const to of options.to) { 481 for (const to of options.to) {
478 await this.transporter.sendMail({ 482 await email
479 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, 483 .send(merge(
480 replyTo: options.replyTo, 484 {
481 to, 485 template: 'common',
482 subject: options.subject, 486 message: {
483 text: options.text 487 to,
484 }) 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)
485 } 503 }
486 } 504 }
487 505
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug
new file mode 100644
index 000000000..9a1894cab
--- /dev/null
+++ b/server/lib/emails/common/base.pug
@@ -0,0 +1,267 @@
1//-
2 The email background color is defined in three places:
3 1. body tag: for most email clients
4 2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
5 3. mso conditional: For Windows 10 Mail
6- var backgroundColor = "#fff";
7- var mainColor = "#f2690d";
8doctype html
9head
10 // This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
11 meta(charset='utf-8')
12 //- utf-8 works for most cases
13 meta(name='viewport' content='width=device-width')
14 //- Forcing initial-scale shouldn't be necessary
15 meta(http-equiv='X-UA-Compatible' content='IE=edge')
16 //- Use the latest (edge) version of IE rendering engine
17 meta(name='x-apple-disable-message-reformatting')
18 //- Disable auto-scale in iOS 10 Mail entirely
19 meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
20 //- Tell iOS not to automatically link certain text strings.
21 meta(name='color-scheme' content='light')
22 meta(name='supported-color-schemes' content='light')
23 //- The title tag shows in email notifications, like Android 4.4.
24 title #{subject}
25 //- What it does: Makes background images in 72ppi Outlook render at correct size.
26 //if gte mso 9
27 xml
28 o:officedocumentsettings
29 o:allowpng
30 o:pixelsperinch 96
31 //- CSS Reset : BEGIN
32 style.
33 /* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
34 :root {
35 color-scheme: light;
36 supported-color-schemes: light;
37 }
38 /* What it does: Remove spaces around the email design added by some email clients. */
39 /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
40 html,
41 body {
42 margin: 0 auto !important;
43 padding: 0 !important;
44 height: 100% !important;
45 width: 100% !important;
46 }
47 /* What it does: Stops email clients resizing small text. */
48 * {
49 -ms-text-size-adjust: 100%;
50 -webkit-text-size-adjust: 100%;
51 }
52 /* What it does: Centers email on Android 4.4 */
53 div[style*="margin: 16px 0"] {
54 margin: 0 !important;
55 }
56 /* What it does: forces Samsung Android mail clients to use the entire viewport */
57 #MessageViewBody, #MessageWebViewDiv{
58 width: 100% !important;
59 }
60 /* What it does: Stops Outlook from adding extra spacing to tables. */
61 table,
62 td {
63 mso-table-lspace: 0pt !important;
64 mso-table-rspace: 0pt !important;
65 }
66 /* What it does: Fixes webkit padding issue. */
67 table {
68 border-spacing: 0 !important;
69 border-collapse: collapse !important;
70 table-layout: fixed !important;
71 margin: 0 auto !important;
72 }
73 /* What it does: Uses a better rendering method when resizing images in IE. */
74 img {
75 -ms-interpolation-mode:bicubic;
76 }
77 /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
78 a {
79 text-decoration: none;
80 }
81 a:not(.nocolor) {
82 color: #{mainColor};
83 }
84 a.nocolor {
85 color: inherit !important;
86 }
87 /* What it does: A work-around for email clients meddling in triggered links. */
88 a[x-apple-data-detectors], /* iOS */
89 .unstyle-auto-detected-links a,
90 .aBn {
91 border-bottom: 0 !important;
92 cursor: default !important;
93 color: inherit !important;
94 text-decoration: none !important;
95 font-size: inherit !important;
96 font-family: inherit !important;
97 font-weight: inherit !important;
98 line-height: inherit !important;
99 }
100 /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
101 .a6S {
102 display: none !important;
103 opacity: 0.01 !important;
104 }
105 /* What it does: Prevents Gmail from changing the text color in conversation threads. */
106 .im {
107 color: inherit !important;
108 }
109 /* If the above doesn't work, add a .g-img class to any image in question. */
110 img.g-img + div {
111 display: none !important;
112 }
113 /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
114 /* Create one of these media queries for each additional viewport size you'd like to fix */
115 /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
116 @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
117 u ~ div .email-container {
118 min-width: 320px !important;
119 }
120 }
121 /* iPhone 6, 6S, 7, 8, and X */
122 @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
123 u ~ div .email-container {
124 min-width: 375px !important;
125 }
126 }
127 /* iPhone 6+, 7+, and 8+ */
128 @media only screen and (min-device-width: 414px) {
129 u ~ div .email-container {
130 min-width: 414px !important;
131 }
132 }
133 //- CSS Reset : END
134 //- CSS for PeerTube : START
135 style.
136 blockquote {
137 margin-left: 0;
138 padding-left: 20px;
139 border-left: 2px solid #f2690d;
140 }
141 //- CSS for PeerTube : END
142 //- Progressive Enhancements : BEGIN
143 style.
144 /* What it does: Hover styles for buttons */
145 .button-td,
146 .button-a {
147 transition: all 100ms ease-in;
148 }
149 .button-td-primary:hover,
150 .button-a-primary:hover {
151 background: #555555 !important;
152 border-color: #555555 !important;
153 }
154 /* Media Queries */
155 @media screen and (max-width: 600px) {
156 /* What it does: Adjust typography on small screens to improve readability */
157 .email-container p {
158 font-size: 17px !important;
159 }
160 }
161 //- Progressive Enhancements : END
162
163body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};")
164 center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};')
165 //if mso | IE
166 table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
167 tr
168 td
169 //- Visually Hidden Preheader Text : BEGIN
170 div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
171 block preheader
172 //- Visually Hidden Preheader Text : END
173
174 //- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary.
175 //- Preview Text Spacing Hack : BEGIN
176 div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;')
177 | &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
178 //- Preview Text Spacing Hack : END
179
180 //-
181 Set the email width. Defined in two places:
182 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
183 2. MSO tags for Desktop Windows Outlook enforce a 600px width.
184 .email-container(style='max-width: 600px; margin: 0 auto;')
185 //if mso
186 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
187 tr
188 td
189 //- Email Body : BEGIN
190 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
191 //- 1 Column Text + Button : BEGIN
192 tr
193 td(style='background-color: #ffffff;')
194 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
195 tr
196 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
197 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
198 tr
199 td(width="40px")
200 img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="icon" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;")
201 td
202 h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;')
203 block title
204 if title
205 | #{title}
206 else
207 | Something requires your attention
208 p(style='margin: 0;')
209 block body
210 if action
211 tr
212 td(style='padding: 0 20px;')
213 //- Button : BEGIN
214 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
215 tr
216 td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;')
217 a.button-a.button-a-primary(href=action.url style='background: #222222; border: 1px solid #000000; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;') #{action.text}
218 //- Button : END
219 //- 1 Column Text + Button : END
220 //- Clear Spacer : BEGIN
221 tr
222 td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
223 br
224 //- Clear Spacer : END
225 //- 1 Column Text : BEGIN
226 if username
227 tr
228 td(style='background-color: #cccccc;')
229 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
230 tr
231 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
232 p(style='margin: 0;')
233 | You are receiving this email as part of your notification settings on #{WEBSERVER.HOST} for your account #{username}.
234 //- 1 Column Text : END
235 //- Email Body : END
236 //- Email Footer : BEGIN
237 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
238 tr
239 td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
240 webversion
241 a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications
242 br
243 tr
244 td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
245 unsubscribe
246 a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile
247 br
248 //- Email Footer : END
249 //if mso
250 //- Full Bleed Background Section : BEGIN
251 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`)
252 tr
253 td
254 .email-container(align='center' style='max-width: 600px; margin: auto;')
255 //if mso
256 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center')
257 tr
258 td
259 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
260 tr
261 td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;')
262 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
263 tr
264 td(valign="top") #[a(href="https://github.com/Chocobozzz/PeerTube" style="color: white !important") PeerTube © 2015-#{new Date().getFullYear()}] #[a(href="https://github.com/Chocobozzz/PeerTube/blob/master/CREDITS.md" style="color: white !important") PeerTube Contributors]
265 //if mso
266 //- Full Bleed Background Section : END
267 //if mso | IE
diff --git a/server/lib/emails/common/greetings.pug b/server/lib/emails/common/greetings.pug
new file mode 100644
index 000000000..5efe29dfb
--- /dev/null
+++ b/server/lib/emails/common/greetings.pug
@@ -0,0 +1,11 @@
1extends base
2
3block body
4 if username
5 p Hi #{username},
6 else
7 p Hi,
8 block content
9 p
10 | Cheers,#[br]
11 | #{EMAIL.BODY.SIGNATURE} \ No newline at end of file
diff --git a/server/lib/emails/common/html.pug b/server/lib/emails/common/html.pug
new file mode 100644
index 000000000..d76168b85
--- /dev/null
+++ b/server/lib/emails/common/html.pug
@@ -0,0 +1,4 @@
1extends greetings
2
3block content
4 p !{text} \ No newline at end of file
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug
new file mode 100644
index 000000000..76b805a24
--- /dev/null
+++ b/server/lib/emails/common/mixins.pug
@@ -0,0 +1,3 @@
1mixin channel(channel)
2 - var handle = `${channel.name}@${channel.host}`
3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file
diff --git a/server/lib/emails/contact-form/html.pug b/server/lib/emails/contact-form/html.pug
new file mode 100644
index 000000000..0073ff78e
--- /dev/null
+++ b/server/lib/emails/contact-form/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | Someone just used the contact form
5
6block content
7 p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]:
8 blockquote(style='white-space: pre-wrap') #{body}
9 p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch. \ No newline at end of file
diff --git a/server/lib/emails/follower-on-channel/html.pug b/server/lib/emails/follower-on-channel/html.pug
new file mode 100644
index 000000000..8a352e90f
--- /dev/null
+++ b/server/lib/emails/follower-on-channel/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New follower on your channel
5
6block content
7 p.
8 Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
9 #[a(href=followerUrl) #{followerName}]. \ No newline at end of file
diff --git a/server/lib/emails/password-create/html.pug b/server/lib/emails/password-create/html.pug
new file mode 100644
index 000000000..45ff3078a
--- /dev/null
+++ b/server/lib/emails/password-create/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | Password creation for your account
5
6block content
7 p.
8 Welcome to #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your PeerTube instance. Your username is: #{username}.
9 Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
10 (this link will expire within seven days). \ No newline at end of file
diff --git a/server/lib/emails/password-reset/html.pug b/server/lib/emails/password-reset/html.pug
new file mode 100644
index 000000000..bb6a9d16b
--- /dev/null
+++ b/server/lib/emails/password-reset/html.pug
@@ -0,0 +1,12 @@
1extends ../common/greetings
2
3block title
4 | Password reset for your account
5
6block content
7 p.
8 A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
9 Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
10 (the link will expire within 1 hour)
11 p.
12 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/user-registered/html.pug b/server/lib/emails/user-registered/html.pug
new file mode 100644
index 000000000..20f62125e
--- /dev/null
+++ b/server/lib/emails/user-registered/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | A new user registered
5
6block content
7 - var mail = user.email || user.pendingEmail;
8 p
9 | User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered.
10 | You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}]. \ No newline at end of file
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug
new file mode 100644
index 000000000..8a4a77703
--- /dev/null
+++ b/server/lib/emails/verify-email/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2
3block title
4 | Account verification
5
6block content
7 p Welcome to PeerTube!
8 p.
9 You just created an account #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your new PeerTube instance.
10 Your username there is: #{username}.
11 p.
12 To start using PeerTube on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] you must verify your email first!
13 Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you: #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
14 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug
new file mode 100644
index 000000000..999c89d26
--- /dev/null
+++ b/server/lib/emails/video-abuse-new/html.pug
@@ -0,0 +1,18 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
10 a(href=videoUrl) #{videoAbuse.video.name}
11 | " by #[+channel(videoAbuse.video.channel)]
12 if videoPublishedAt
13 | , published the #{videoPublishedAt}.
14 else
15 | , uploaded the #{videoCreatedAt} but not yet published.
16 p The reporter, #{reporter}, cited the following reason(s):
17 blockquote #{videoAbuse.reason}
18 br(style="display: none;")
diff --git a/server/lib/emails/video-auto-blacklist-new/html.pug b/server/lib/emails/video-auto-blacklist-new/html.pug
new file mode 100644
index 000000000..07c8dfd16
--- /dev/null
+++ b/server/lib/emails/video-auto-blacklist-new/html.pug
@@ -0,0 +1,17 @@
1extends ../common/greetings
2include ../common/mixins
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | A recently added video was auto-blacklisted and requires moderator review before going public:
10 |
11 a(href=videoUrl) #{videoName}
12 |
13 | by #[+channel(channel)].
14 p.
15 Apart from the publisher and the moderation team, no one will be able to see the video until you
16 unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so
17 that they don't require approval before going public.
diff --git a/server/lib/emails/video-comment-mention/html.pug b/server/lib/emails/video-comment-mention/html.pug
new file mode 100644
index 000000000..9e9ced62d
--- /dev/null
+++ b/server/lib/emails/video-comment-mention/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone mentioned you
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/emails/video-comment-new/html.pug b/server/lib/emails/video-comment-new/html.pug
new file mode 100644
index 000000000..075af5717
--- /dev/null
+++ b/server/lib/emails/video-comment-new/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone commented your video
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] added a comment on your video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 710c2d30f..017739523 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -5,7 +5,7 @@ import { UserNotificationModel } from '../models/account/user-notification'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { PeerTubeSocket } from './peertube-socket' 6import { PeerTubeSocket } from './peertube-socket'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { VideoPrivacy, VideoState } from '../../shared/models/videos' 8import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos'
9import { AccountBlocklistModel } from '../models/account/account-blocklist' 9import { AccountBlocklistModel } from '../models/account/account-blocklist'
10import { 10import {
11 MCommentOwnerVideo, 11 MCommentOwnerVideo,
@@ -77,9 +77,9 @@ class Notifier {
77 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 77 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
78 } 78 }
79 79
80 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void { 80 notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
81 this.notifyModeratorsOfNewVideoAbuse(videoAbuse) 81 this.notifyModeratorsOfNewVideoAbuse(parameters)
82 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 82 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
83 } 83 }
84 84
85 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { 85 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
@@ -350,11 +350,15 @@ class Notifier {
350 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 350 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
351 } 351 }
352 352
353 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { 353 private async notifyModeratorsOfNewVideoAbuse (parameters: {
354 videoAbuse: VideoAbuse
355 videoAbuseInstance: MVideoAbuseVideo
356 reporter: string
357 }) {
354 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 358 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
355 if (moderators.length === 0) return 359 if (moderators.length === 0) return
356 360
357 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) 361 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
358 362
359 function settingGetter (user: MUserWithNotificationSetting) { 363 function settingGetter (user: MUserWithNotificationSetting) {
360 return user.NotificationSetting.videoAbuseAsModerator 364 return user.NotificationSetting.videoAbuseAsModerator
@@ -364,15 +368,15 @@ class Notifier {
364 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 368 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
365 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 369 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
366 userId: user.id, 370 userId: user.id,
367 videoAbuseId: videoAbuse.id 371 videoAbuseId: parameters.videoAbuse.id
368 }) 372 })
369 notification.VideoAbuse = videoAbuse 373 notification.VideoAbuse = parameters.videoAbuseInstance
370 374
371 return notification 375 return notification
372 } 376 }
373 377
374 function emailSender (emails: string[]) { 378 function emailSender (emails: string[]) {
375 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) 379 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
376 } 380 }
377 381
378 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 382 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index bd1b0e38a..8d1270358 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -46,7 +46,7 @@ describe('Test contact form', function () {
46 const email = emails[0] 46 const email = emails[0]
47 47
48 expect(email['from'][0]['address']).equal('test-admin@localhost') 48 expect(email['from'][0]['address']).equal('test-admin@localhost')
49 expect(email['from'][0]['name']).equal('toto@example.com') 49 expect(email['replyTo'][0]['address']).equal('toto@example.com')
50 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com') 50 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
51 expect(email['subject']).contains('my subject') 51 expect(email['subject']).contains('my subject')
52 expect(email['text']).contains('my super message') 52 expect(email['text']).contains('my super message')