diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-05-05 20:22:22 +0200 |
---|---|---|
committer | Rigel Kent <par@rigelk.eu> | 2020-05-08 15:31:51 +0200 |
commit | df4c603dea022146476812cbbc2b9f8f1e5e4870 (patch) | |
tree | c0d27576fb6711b4b64d2186e8dca3f04b9b1dfe /server/lib | |
parent | 91b8e675e26dd65e1ebb23706cb16b3a3f8bcf73 (diff) | |
download | PeerTube-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/lib')
-rw-r--r-- | server/lib/activitypub/process/process-flag.ts | 17 | ||||
-rw-r--r-- | server/lib/emailer.ts | 392 | ||||
-rw-r--r-- | server/lib/emails/common/base.pug | 267 | ||||
-rw-r--r-- | server/lib/emails/common/greetings.pug | 11 | ||||
-rw-r--r-- | server/lib/emails/common/html.pug | 4 | ||||
-rw-r--r-- | server/lib/emails/common/mixins.pug | 3 | ||||
-rw-r--r-- | server/lib/emails/contact-form/html.pug | 9 | ||||
-rw-r--r-- | server/lib/emails/follower-on-channel/html.pug | 9 | ||||
-rw-r--r-- | server/lib/emails/password-create/html.pug | 10 | ||||
-rw-r--r-- | server/lib/emails/password-reset/html.pug | 12 | ||||
-rw-r--r-- | server/lib/emails/user-registered/html.pug | 10 | ||||
-rw-r--r-- | server/lib/emails/verify-email/html.pug | 14 | ||||
-rw-r--r-- | server/lib/emails/video-abuse-new/html.pug | 18 | ||||
-rw-r--r-- | server/lib/emails/video-auto-blacklist-new/html.pug | 17 | ||||
-rw-r--r-- | server/lib/emails/video-comment-mention/html.pug | 11 | ||||
-rw-r--r-- | server/lib/emails/video-comment-new/html.pug | 11 | ||||
-rw-r--r-- | server/lib/notifier.ts | 22 |
17 files changed, 637 insertions, 200 deletions
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' | |||
8 | import { Notifier } from '../../notifier' | 8 | import { Notifier } from '../../notifier' |
9 | import { getAPId } from '../../../helpers/activitypub' | 9 | import { getAPId } from '../../../helpers/activitypub' |
10 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 10 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
11 | import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models' | 11 | import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models' |
12 | import { AccountModel } from '@server/models/account/account' | ||
12 | 13 | ||
13 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 14 | async 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 @@ | |||
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, isEmailEnabled } from '../initializers/config' | 4 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
5 | import { JobQueue } from './job-queue' | 5 | import { JobQueue } from './job-queue' |
@@ -16,6 +16,12 @@ import { | |||
16 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' | 16 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' |
17 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' | 17 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' |
18 | import { EmailPayload } from '@shared/models' | 18 | import { EmailPayload } from '@shared/models' |
19 | import { join } from 'path' | ||
20 | import { VideoAbuse } from '../../shared/models/videos' | ||
21 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | ||
22 | import { merge } from 'lodash' | ||
23 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
24 | const Email = require('email-templates') | ||
19 | 25 | ||
20 | class Emailer { | 26 | class 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"; | ||
8 | doctype html | ||
9 | head | ||
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 | |||
163 | body(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 | | ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ | ||
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 @@ | |||
1 | extends base | ||
2 | |||
3 | block 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 @@ | |||
1 | extends greetings | ||
2 | |||
3 | block 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 @@ | |||
1 | mixin 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone just used the contact form | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New follower on your channel | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Password creation for your account | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Password reset for your account | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | A new user registered | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Account verification | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | A video is pending moderation | ||
6 | |||
7 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins | ||
3 | |||
4 | block title | ||
5 | | A video is pending moderation | ||
6 | |||
7 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone mentioned you | ||
5 | |||
6 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone commented your video | ||
5 | |||
6 | block 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' | |||
5 | import { UserModel } from '../models/account/user' | 5 | import { UserModel } from '../models/account/user' |
6 | import { PeerTubeSocket } from './peertube-socket' | 6 | import { PeerTubeSocket } from './peertube-socket' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | 8 | import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos' |
9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
10 | import { | 10 | import { |
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 }) |