Browse Source

Switch emails to pug templates and provide richer html/text-only versions

tags/v2.2.0-rc.1
Rigel Kent Rigel Kent 5 months ago
parent
commit
df4c603dea
30 changed files with 1682 additions and 274 deletions
  1. +1
    -1
      client/src/app/+admin/follows/followers-list/followers-list.component.html
  2. +1
    -1
      client/src/app/+admin/follows/following-list/following-list.component.html
  3. +1
    -1
      client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
  4. +1
    -1
      client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
  5. +1
    -1
      client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
  6. +1
    -1
      client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
  7. +7
    -7
      client/src/app/+admin/users/user-list/user-list.component.html
  8. +2
    -0
      package.json
  9. +14
    -6
      server/controllers/api/videos/abuse.ts
  10. +13
    -4
      server/lib/activitypub/process/process-flag.ts
  11. +205
    -187
      server/lib/emailer.ts
  12. +267
    -0
      server/lib/emails/common/base.pug
  13. +11
    -0
      server/lib/emails/common/greetings.pug
  14. +4
    -0
      server/lib/emails/common/html.pug
  15. +3
    -0
      server/lib/emails/common/mixins.pug
  16. +9
    -0
      server/lib/emails/contact-form/html.pug
  17. +9
    -0
      server/lib/emails/follower-on-channel/html.pug
  18. +10
    -0
      server/lib/emails/password-create/html.pug
  19. +12
    -0
      server/lib/emails/password-reset/html.pug
  20. +10
    -0
      server/lib/emails/user-registered/html.pug
  21. +14
    -0
      server/lib/emails/verify-email/html.pug
  22. +18
    -0
      server/lib/emails/video-abuse-new/html.pug
  23. +17
    -0
      server/lib/emails/video-auto-blacklist-new/html.pug
  24. +11
    -0
      server/lib/emails/video-comment-mention/html.pug
  25. +11
    -0
      server/lib/emails/video-comment-new/html.pug
  26. +13
    -9
      server/lib/notifier.ts
  27. +1
    -1
      server/tests/api/server/contact-form.ts
  28. +30
    -30
      shared/extra-utils/users/user-notifications.ts
  29. +7
    -3
      shared/models/server/emailer.model.ts
  30. +978
    -21
      yarn.lock

+ 1
- 1
client/src/app/+admin/follows/followers-list/followers-list.component.html View File

@@ -22,7 +22,7 @@
<th i18n>Follower handle</th>
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 100px;"></th>
</tr>
</ng-template>


+ 1
- 1
client/src/app/+admin/follows/following-list/following-list.component.html View File

@@ -25,7 +25,7 @@
<tr>
<th i18n>Host</th>
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
<th style="width: 100px;"></th>
</tr>


+ 1
- 1
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html View File

@@ -20,7 +20,7 @@
<ng-template pTemplate="header">
<tr>
<th style="width: 100%;" i18n>Account</th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 100px;"></th> <!-- column for action buttons -->
</tr>
</ng-template>


+ 1
- 1
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html View File

@@ -24,7 +24,7 @@
<ng-template pTemplate="header">
<tr>
<th style="width: 100%;" i18n>Instance</th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 100px;"></th> <!-- column for action buttons -->
</tr>
</ng-template>


+ 1
- 1
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html View File

@@ -39,7 +39,7 @@
<th style="width: 40px;"></th>
<th style="width: 20%;" pResizableColumn i18n>Reporter</th>
<th i18n>Video</th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
<th style="width: 120px;"></th>
</tr>


+ 1
- 1
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html View File

@@ -24,7 +24,7 @@
<th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th>
<th style="width: 100px;" i18n>Sensitive</th>
<th style="width: 120px;" i18n>Unfederated</th>
<th style="width: 140px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 120px;"></th>
</tr>
</ng-template>


+ 7
- 7
client/src/app/+admin/users/user-list/user-list.component.html View File

@@ -9,7 +9,7 @@

<p-table
[value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
[(selection)]="selectedUsers"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
@@ -42,12 +42,12 @@
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
</th>
<th style="width: 40px"></th>
<th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
<th pResizableColumn i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
<th i18n>Email</th>
<th i18n pSortableColumn="videoQuotaUsed">Video quota <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th>
<th i18n>Role</th>
<th i18n>Auth plugin</th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 140px;" i18n pSortableColumn="videoQuotaUsed">Video quota <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th>
<th style="width: 120px;" i18n>Role</th>
<th style="width: 140px;" pResizableColumn i18n>Auth plugin</th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 50px;"></th>
</tr>
</ng-template>
@@ -103,7 +103,7 @@
<ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container>
</td>

<td [title]="user.createdAt">{{ user.createdAt }}</td>
<td [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td>

<td class="action-cell">
<my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">


+ 2
- 0
package.json View File

@@ -98,6 +98,7 @@
"cors": "^2.8.1",
"create-torrent": "^4.0.0",
"deep-object-diff": "^1.1.0",
"email-templates": "^7.0.4",
"express": "^4.12.4",
"express-oauth-server": "^2.0.0",
"express-rate-limit": "^5.0.0",
@@ -127,6 +128,7 @@
"pfeed": "1.1.11",
"pg": "^7.4.1",
"prompt": "^1.0.0",
"pug": "^2.0.4",
"redis": "^3.0.2",
"reflect-metadata": "^0.1.12",
"request": "^2.81.0",


+ 14
- 6
server/controllers/api/videos/abuse.ts View File

@@ -1,5 +1,5 @@
import * as express from 'express'
import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared'
import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import { sequelizeTypescript } from '../../../initializers/database'
@@ -24,6 +24,7 @@ import { Notifier } from '../../../lib/notifier'
import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
import { MVideoAbuseAccountVideo } from '../../../typings/models/video'
import { getServerActor } from '@server/models/application/application'
import { MAccountDefault } from '@server/typings/models'

const auditLogger = auditLoggerFactory('abuse')
const abuseVideoRouter = express.Router()
@@ -117,9 +118,11 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
async function reportVideoAbuse (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
const body: VideoAbuseCreate = req.body
let reporterAccount: MAccountDefault
let videoAbuseJSON: VideoAbuse

const videoAbuse = await sequelizeTypescript.transaction(async t => {
const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)

const abuseToCreate = {
reporterAccountId: reporterAccount.id,
@@ -137,14 +140,19 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
}

auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON()))
videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))

return videoAbuseInstance
})

Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse)
Notifier.Instance.notifyOnNewVideoAbuse({
videoAbuse: videoAbuseJSON,
videoAbuseInstance,
reporter: reporterAccount.Actor.getIdentifier()
})

logger.info('Abuse report for video %s created.', videoInstance.name)

return res.json({ videoAbuse: videoAbuse.toFormattedJSON() }).end()
return res.json({ videoAbuseJSON }).end()
}

+ 13
- 4
server/lib/activitypub/process/process-flag.ts View File

@@ -8,7 +8,8 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { Notifier } from '../../notifier'
import { getAPId } from '../../../helpers/activitypub'
import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models'
import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models'
import { AccountModel } from '@server/models/account/account'

async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
const { activity, byActor } = options
@@ -36,8 +37,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
logger.debug('Reporting remote abuse for video %s.', getAPId(object))

const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))

const videoAbuse = await sequelizeTypescript.transaction(async t => {
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
const videoAbuseData = {
reporterAccountId: account.id,
reason: flag.content,
@@ -45,15 +47,22 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
state: VideoAbuseState.PENDING
}

const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
videoAbuseInstance.Video = video
videoAbuseInstance.Account = reporterAccount

logger.info('Remote abuse for video uuid %s created', flag.object)

return videoAbuseInstance
})

Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse)
const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()

Notifier.Instance.notifyOnNewVideoAbuse({
videoAbuse: videoAbuseJSON,
videoAbuseInstance,
reporter: reporterAccount.Actor.getIdentifier()
})
} catch (err) {
logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
}


+ 205
- 187
server/lib/emailer.ts View File

@@ -1,5 +1,5 @@
import { createTransport, Transporter } from 'nodemailer'
import { isTestInstance } from '../helpers/core-utils'
import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config'
import { JobQueue } from './job-queue'
@@ -16,6 +16,12 @@ import {
import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
import { EmailPayload } from '@shared/models'
import { join } from 'path'
import { VideoAbuse } from '../../shared/models/videos'
import { SendEmailOptions } from '../../shared/models/server/emailer.model'
import { merge } from 'lodash'
import { VideoChannelModel } from '@server/models/video/video-channel'
const Email = require('email-templates')

class Emailer {

@@ -105,37 +111,36 @@ class Emailer {
const channelName = video.VideoChannel.getDisplayName()
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()

const text = 'Hi dear user,\n\n' +
`Your subscription ${channelName} just published a new video: ${video.name}` +
'\n\n' +
`You can view it on ${videoUrl} ` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video',
text
subject: channelName + ' just published a new video',
text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
locals: {
title: 'New content ',
action: {
text: 'View video',
url: videoUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
const followerName = actorFollow.ActorFollower.Account.getDisplayName()
const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()

const text = 'Hi dear user,\n\n' +
`Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'follower-on-channel',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName,
text
subject: `New follower on your channel ${followingName}`,
locals: {
followerName: actorFollow.ActorFollower.Account.getDisplayName(),
followerUrl: actorFollow.ActorFollower.url,
followingName,
followingUrl: actorFollow.ActorFollowing.url,
followType
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -144,32 +149,28 @@ class Emailer {
addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''

const text = 'Hi dear admin,\n\n' +
`Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower',
text
subject: 'New instance follower',
text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
locals: {
title: 'New instance follower',
action: {
text: 'Review followers',
url: WEBSERVER.URL + '/admin/follows/followers-list'
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
const text = 'Hi dear admin,\n\n' +
`Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const instanceUrl = actorFollow.ActorFollowing.url
const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
text
subject: 'Auto instance following',
text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -178,18 +179,17 @@ class Emailer {
myVideoPublishedNotification (to: string[], video: MVideo) {
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()

const text = 'Hi dear user,\n\n' +
`Your video ${video.name} has been published.` +
'\n\n' +
`You can view it on ${videoUrl} ` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`,
text
subject: `Your video ${video.name} has been published`,
text: `Your video "${video.name}" has been published.`,
locals: {
title: 'You video is live',
action: {
text: 'View video',
url: videoUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -198,18 +198,17 @@ class Emailer {
myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()

const text = 'Hi dear user,\n\n' +
`Your video import ${videoImport.getTargetIdentifier()} is finished.` +
'\n\n' +
`You can view the imported video on ${videoUrl} ` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`,
text
subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
locals: {
title: 'Import complete',
action: {
text: 'View video',
url: videoUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -218,40 +217,47 @@ class Emailer {
myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
const importUrl = WEBSERVER.URL + '/my-account/video-imports'

const text = 'Hi dear user,\n\n' +
`Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
'\n\n' +
`See your videos import dashboard for more information: ${importUrl}` +
const text =
`Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`
`See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
text
subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
text,
locals: {
title: 'Import failed',
action: {
text: 'Review imports',
url: importUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
const accountName = comment.Account.getDisplayName()
const video = comment.Video
const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()

const text = 'Hi dear user,\n\n' +
`A new comment has been posted by ${accountName} on your video ${video.name}` +
'\n\n' +
`You can view it on ${commentUrl} ` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'video-comment-new',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name,
text
subject: 'New comment on your video ' + video.name,
locals: {
accountName: comment.Account.getDisplayName(),
accountUrl: comment.Account.Actor.url,
comment,
video,
videoUrl,
action: {
text: 'View comment',
url: commentUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -260,75 +266,88 @@ class Emailer {
addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
const accountName = comment.Account.getDisplayName()
const video = comment.Video
const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()

const text = 'Hi dear user,\n\n' +
`${accountName} mentioned you on video ${video.name}` +
'\n\n' +
`You can view the comment on ${commentUrl} ` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'video-comment-mention',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name,
text
subject: 'Mention on video ' + video.name,
locals: {
comment,
video,
videoUrl,
accountName,
action: {
text: 'View comment',
url: commentUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) {
const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()

const text = 'Hi,\n\n' +
`${WEBSERVER.HOST} received an abuse for the following video: ${videoUrl}\n\n` +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`
addVideoAbuseModeratorsNotification (to: string[], parameters: {
videoAbuse: VideoAbuse
videoAbuseInstance: MVideoAbuseVideo
reporter: string
}) {
const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()

const emailPayload: EmailPayload = {
template: 'video-abuse-new',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse',
text
subject: `New video abuse report from ${parameters.reporter}`,
locals: {
videoUrl,
videoAbuseUrl,
videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
videoAbuse: parameters.videoAbuse,
reporter: parameters.reporter,
action: {
text: 'View report #' + parameters.videoAbuse.id,
url: videoAbuseUrl
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()

const text = 'Hi,\n\n' +
'A recently added video was auto-blacklisted and requires moderator review before publishing.' +
'\n\n' +
`You can view it and take appropriate action on ${videoUrl}` +
'\n\n' +
`A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`
const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()

const emailPayload: EmailPayload = {
template: 'video-auto-blacklist-new',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review',
text
subject: 'A new video is pending moderation',
locals: {
channel,
videoUrl,
videoName: videoBlacklist.Video.name,
action: {
text: 'Review autoblacklist',
url: VIDEO_AUTO_BLACKLIST_URL
}
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addNewUserRegistrationNotification (to: string[], user: MUser) {
const text = 'Hi,\n\n' +
`User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'user-registered',
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST,
text
subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
locals: {
user
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -341,16 +360,13 @@ class Emailer {
const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`

const text = 'Hi,\n\n' +
blockedString +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`,
text
subject: `Video ${videoName} blacklisted`,
text: blockedString,
locals: {
title: 'Your video was blacklisted'
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -359,66 +375,53 @@ class Emailer {
addVideoUnblacklistNotification (to: string[], video: MVideo) {
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()

const text = 'Hi,\n\n' +
`Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
to,
subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`,
text
subject: `Video ${video.name} unblacklisted`,
text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
locals: {
title: 'Your video was unblacklisted'
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
const text = 'Hi dear user,\n\n' +
`A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
`Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` +
'If you are not the person who initiated this request, please ignore this email.\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'password-reset',
to: [ to ],
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password',
text
subject: 'Reset your account password',
locals: {
resetPasswordUrl
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) {
const text = 'Hi,\n\n' +
`Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` +
`Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
const emailPayload: EmailPayload = {
template: 'password-create',
to: [ to ],
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password',
text
subject: 'Create your account password',
locals: {
username,
createPasswordUrl
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addVerifyEmailJob (to: string, verifyEmailUrl: string) {
const text = 'Welcome to PeerTube,\n\n' +
`To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
`Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
'If you are not the person who initiated this request, please ignore this email.\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const emailPayload: EmailPayload = {
template: 'verify-email',
to: [ to ],
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email',
text
subject: `Verify your email on ${WEBSERVER.HOST}`,
locals: {
verifyEmailUrl
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -427,39 +430,28 @@ class Emailer {
addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
const reasonString = reason ? ` for the following reason: ${reason}` : ''
const blockedWord = blocked ? 'blocked' : 'unblocked'
const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`

const text = 'Hi,\n\n' +
blockedString +
'\n\n' +
'Cheers,\n' +
`${CONFIG.EMAIL.BODY.SIGNATURE}`

const to = user.email
const emailPayload: EmailPayload = {
to: [ to ],
subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord,
text
subject: 'Account ' + blockedWord,
text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}

addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
const text = 'Hello dear admin,\n\n' +
fromName + ' sent you a message' +
'\n\n---------------------------------------\n\n' +
body +
'\n\n---------------------------------------\n\n' +
'Cheers,\n' +
'PeerTube.'

const emailPayload: EmailPayload = {
fromDisplayName: fromEmail,
replyTo: fromEmail,
template: 'contact-form',
to: [ CONFIG.ADMIN.EMAIL ],
subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject,
text
replyTo: `"${fromName}" <${fromEmail}>`,
subject: `(contact form) ${subject}`,
locals: {
fromName,
fromEmail,
body
}
}

return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -470,18 +462,44 @@ class Emailer {
throw new Error('Cannot send mail because SMTP is not configured.')
}

const fromDisplayName = options.fromDisplayName
? options.fromDisplayName
const fromDisplayName = options.from
? options.from
: WEBSERVER.HOST

const email = new Email({
send: true,
message: {
from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
},
transport: this.transporter,
views: {
root: join(root(), 'server', 'lib', 'emails')
},
subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
})

for (const to of options.to) {
await this.transporter.sendMail({
from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`,
replyTo: options.replyTo,
to,
subject: options.subject,
text: options.text
})
await email
.send(merge(
{
template: 'common',
message: {
to,
from: options.from,
subject: options.subject,
replyTo: options.replyTo
},
locals: { // default variables available in all templates
WEBSERVER,
EMAIL: CONFIG.EMAIL,
text: options.text,
subject: options.subject
}
},
options // overriden/new variables given for a specific template in the payload
) as SendEmailOptions)
.then(logger.info)
.catch(logger.error)
}
}



+ 267
- 0
server/lib/emails/common/base.pug View File

@@ -0,0 +1,267 @@
//-
The email background color is defined in three places:
1. body tag: for most email clients
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
3. mso conditional: For Windows 10 Mail
- var backgroundColor = "#fff";
- var mainColor = "#f2690d";
doctype html
head
// This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
meta(charset='utf-8')
//- utf-8 works for most cases
meta(name='viewport' content='width=device-width')
//- Forcing initial-scale shouldn't be necessary
meta(http-equiv='X-UA-Compatible' content='IE=edge')
//- Use the latest (edge) version of IE rendering engine
meta(name='x-apple-disable-message-reformatting')
//- Disable auto-scale in iOS 10 Mail entirely
meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
//- Tell iOS not to automatically link certain text strings.
meta(name='color-scheme' content='light')
meta(name='supported-color-schemes' content='light')
//- The title tag shows in email notifications, like Android 4.4.
title #{subject}
//- What it does: Makes background images in 72ppi Outlook render at correct size.
//if gte mso 9
xml
o:officedocumentsettings
o:allowpng
o:pixelsperinch 96
//- CSS Reset : BEGIN
style.
/* 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. */
:root {
color-scheme: light;
supported-color-schemes: light;
}
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: forces Samsung Android mail clients to use the entire viewport */
#MessageViewBody, #MessageWebViewDiv{
width: 100% !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode:bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
a:not(.nocolor) {
color: #{mainColor};
}
a.nocolor {
color: inherit !important;
}
/* What it does: A work-around for email clients meddling in triggered links. */
a[x-apple-data-detectors], /* iOS */
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u ~ div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u ~ div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u ~ div .email-container {
min-width: 414px !important;
}
}
//- CSS Reset : END
//- CSS for PeerTube : START
style.
blockquote {
margin-left: 0;
padding-left: 20px;
border-left: 2px solid #f2690d;
}
//- CSS for PeerTube : END
//- Progressive Enhancements : BEGIN
style.
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #555555 !important;
border-color: #555555 !important;
}
/* Media Queries */
@media screen and (max-width: 600px) {
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
}
}
//- Progressive Enhancements : END

body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};")
center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};')
//if mso | IE
table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
tr
td
//- Visually Hidden Preheader Text : BEGIN
div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
block preheader
//- Visually Hidden Preheader Text : END

//- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary.
//- Preview Text Spacing Hack : BEGIN
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;')
| &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;
//- Preview Text Spacing Hack : END

//-
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
2. MSO tags for Desktop Windows Outlook enforce a 600px width.
.email-container(style='max-width: 600px; margin: 0 auto;')
//if mso
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
tr
td
//- Email Body : BEGIN
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
//- 1 Column Text + Button : BEGIN
tr
td(style='background-color: #ffffff;')
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
tr
td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
tr
td(width="40px")
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;")
td
h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;')
block title
if title
| #{title}
else
| Something requires your attention
p(style='margin: 0;')
block body
if action
tr
td(style='padding: 0 20px;')
//- Button : BEGIN
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
tr
td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;')
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}
//- Button : END
//- 1 Column Text + Button : END
//- Clear Spacer : BEGIN
tr
td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
br
//- Clear Spacer : END
//- 1 Column Text : BEGIN
if username
tr
td(style='background-color: #cccccc;')
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
tr
td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
p(style='margin: 0;')
| You are receiving this email as part of your notification settings on #{WEBSERVER.HOST} for your account #{username}.
//- 1 Column Text : END
//- Email Body : END
//- Email Footer : BEGIN
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
tr
td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
webversion
a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications
br
tr
td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
unsubscribe
a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile
br
//- Email Footer : END
//if mso
//- Full Bleed Background Section : BEGIN
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`)
tr
td
.email-container(align='center' style='max-width: 600px; margin: auto;')
//if mso
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center')
tr
td
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
tr
td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;')
table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
tr
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]
//if mso
//- Full Bleed Background Section : END
//if mso | IE

+ 11
- 0
server/lib/emails/common/greetings.pug View File

@@ -0,0 +1,11 @@
extends base

block body
if username
p Hi #{username},
else
p Hi,
block content
p
| Cheers,#[br]
| #{EMAIL.BODY.SIGNATURE}

+ 4
- 0
server/lib/emails/common/html.pug View File

@@ -0,0 +1,4 @@
extends greetings

block content
p !{text}

+ 3
- 0
server/lib/emails/common/mixins.pug View File

@@ -0,0 +1,3 @@
mixin channel(channel)
- var handle = `${channel.name}@${channel.host}`
| #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]

+ 9
- 0
server/lib/emails/contact-form/html.pug View File

@@ -0,0 +1,9 @@
extends ../common/greetings

block title
| Someone just used the contact form

block content
p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]:
blockquote(style='white-space: pre-wrap') #{body}
p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch.

+ 9
- 0
server/lib/emails/follower-on-channel/html.pug View File

@@ -0,0 +1,9 @@
extends ../common/greetings

block title
| New follower on your channel

block content
p.
Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
#[a(href=followerUrl) #{followerName}].

+ 10
- 0
server/lib/emails/password-create/html.pug View File

@@ -0,0 +1,10 @@
extends ../common/greetings

block title
| Password creation for your account

block content
p.
Welcome to #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your PeerTube instance. Your username is: #{username}.
Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
(this link will expire within seven days).

+ 12
- 0
server/lib/emails/password-reset/html.pug View File

@@ -0,0 +1,12 @@
extends ../common/greetings

block title
| Password reset for your account

block content
p.
A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
(the link will expire within 1 hour)
p.
If you are not the person who initiated this request, please ignore this email.

+ 10
- 0
server/lib/emails/user-registered/html.pug View File

@@ -0,0 +1,10 @@
extends ../common/greetings

block title
| A new user registered

block content
- var mail = user.email || user.pendingEmail;
p
| User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered.
| You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}].

+ 14
- 0
server/lib/emails/verify-email/html.pug View File

@@ -0,0 +1,14 @@
extends ../common/greetings

block title
| Account verification

block content
p Welcome to PeerTube!
p.
You just created an account #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your new PeerTube instance.
Your username there is: #{username}.
p.
To start using PeerTube on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] you must verify your email first!
Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you: #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
If you are not the person who initiated this request, please ignore this email.

+ 18
- 0
server/lib/emails/video-abuse-new/html.pug View File

@@ -0,0 +1,18 @@
extends ../common/greetings
include ../common/mixins.pug

block title
| A video is pending moderation

block content
p
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
a(href=videoUrl) #{videoAbuse.video.name}
| " by #[+channel(videoAbuse.video.channel)]
if videoPublishedAt
| , published the #{videoPublishedAt}.
else
| , uploaded the #{videoCreatedAt} but not yet published.
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{videoAbuse.reason}
br(style="display: none;")

+ 17
- 0
server/lib/emails/video-auto-blacklist-new/html.pug View File

@@ -0,0 +1,17 @@
extends ../common/greetings
include ../common/mixins

block title
| A video is pending moderation

block content
p
| A recently added video was auto-blacklisted and requires moderator review before going public:
|
a(href=videoUrl) #{videoName}
|
| by #[+channel(channel)].
p.
Apart from the publisher and the moderation team, no one will be able to see the video until you
unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so
that they don't require approval before going public.

+ 11
- 0
server/lib/emails/video-comment-mention/html.pug View File

@@ -0,0 +1,11 @@
extends ../common/greetings

block title
| Someone mentioned you

block content
p.
#[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video
"#[a(href=videoUrl) #{video.name}]":
blockquote #{comment.text}
br(style="display: none;")

+ 11
- 0
server/lib/emails/video-comment-new/html.pug View File

@@ -0,0 +1,11 @@
extends ../common/greetings

block title
| Someone commented your video

block content
p.
#[a(href=accountUrl title=handle) #{accountName}] added a comment on your video
"#[a(href=videoUrl) #{video.name}]":
blockquote #{comment.text}
br(style="display: none;")

+ 13
- 9
server/lib/notifier.ts View File

@@ -5,7 +5,7 @@ import { UserNotificationModel } from '../models/account/user-notification'
import { UserModel } from '../models/account/user'
import { PeerTubeSocket } from './peertube-socket'
import { CONFIG } from '../initializers/config'
import { VideoPrivacy, VideoState } from '../../shared/models/videos'
import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import {
MCommentOwnerVideo,
@@ -77,9 +77,9 @@ class Notifier {
.catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
}

notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void {
this.notifyModeratorsOfNewVideoAbuse(videoAbuse)
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
this.notifyModeratorsOfNewVideoAbuse(parameters)
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
}

notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
@@ -350,11 +350,15 @@ class Notifier {
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
}

private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) {
private async notifyModeratorsOfNewVideoAbuse (parameters: {
videoAbuse: VideoAbuse
videoAbuseInstance: MVideoAbuseVideo
reporter: string
}) {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
if (moderators.length === 0) return

logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url)
logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)

function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.videoAbuseAsModerator
@@ -364,15 +368,15 @@ class Notifier {
const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
userId: user.id,
videoAbuseId: videoAbuse.id
videoAbuseId: parameters.videoAbuse.id
})
notification.VideoAbuse = videoAbuse
notification.VideoAbuse = parameters.videoAbuseInstance

return notification
}

function emailSender (emails: string[]) {
return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse)
return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
}

return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })


+ 1
- 1
server/tests/api/server/contact-form.ts View File

@@ -46,7 +46,7 @@ describe('Test contact form', function () {
const email = emails[0]

expect(email['from'][0]['address']).equal('test-admin@localhost')
expect(email['from'][0]['name']).equal('toto@example.com')
expect(email['replyTo'][0]['address']).equal('toto@example.com')
expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
expect(email['subject']).contains('my subject')
expect(email['text']).contains('my super message')


+ 30
- 30
shared/extra-utils/users/user-notifications.ts View File

@@ -110,10 +110,10 @@ async function checkNotification (

if (checkType === 'presence') {
const obj = inspect(base.socketNotifications, { depth: 5 })
expect(socketNotification, 'The socket notification is absent. ' + obj).to.not.be.undefined
expect(socketNotification, 'The socket notification is absent when is should be present. ' + obj).to.not.be.undefined
} else {
const obj = inspect(socketNotification, { depth: 5 })
expect(socketNotification, 'The socket notification is present. ' + obj).to.be.undefined
expect(socketNotification, 'The socket notification is present when is should not be present. ' + obj).to.be.undefined
}
}

@@ -125,9 +125,9 @@ async function checkNotification (
.find(e => emailNotificationFinder(e))

if (checkType === 'presence') {
expect(email, 'The email is absent. ' + inspect(base.emails)).to.not.be.undefined
expect(email, 'The email is absent when is should be present. ' + inspect(base.emails)).to.not.be.undefined
} else {
expect(email, 'The email is present. ' + inspect(email)).to.be.undefined
expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
}
}
}
@@ -172,12 +172,12 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text = email['text']
return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
@@ -195,12 +195,12 @@ async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']
return text.includes(videoUUID) && text.includes('Your video')
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkMyVideoImportIsFinished (
@@ -226,14 +226,14 @@ async function checkMyVideoImportIsFinished (
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']
const toFind = success ? ' finished' : ' error'

return text.includes(url) && text.includes(toFind)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
@@ -251,13 +251,13 @@ async function checkUserRegistered (base: CheckerBaseParams, username: string, t
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']

return text.includes(' registered ') && text.includes(username)
return text.includes(' registered.') && text.includes(username)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkNewActorFollow (
@@ -291,13 +291,13 @@ async function checkNewActorFollow (
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']

return text.includes('Your ' + followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost: string, type: CheckerType) {
@@ -320,13 +320,13 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']

return text.includes('instance has a new follower') && text.includes(followerHost)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
@@ -351,13 +351,13 @@ async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']

return text.includes(' automatically followed a new instance') && text.includes(followingHost)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkCommentMention (
@@ -385,13 +385,13 @@ async function checkCommentMention (
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text: string = email['text']

return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

let lastEmailCount = 0
@@ -416,11 +416,11 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string,

const commentUrl = `http://localhost:${base.server.port}/videos/watch/${uuid};threadId=${threadId}`

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
return email['text'].indexOf(commentUrl) !== -1
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)

if (type === 'presence') {
// We cannot detect email duplicates, so check we received another email
@@ -446,12 +446,12 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text = email['text']
return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
@@ -471,12 +471,12 @@ async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, vi
}
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text = email['text']
return text.indexOf(videoUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
}

await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}

async function checkNewBlacklistOnMyVideo (
@@ -498,12 +498,12 @@ async function checkNewBlacklistOnMyVideo (
checkVideo(video, videoName, videoUUID)
}

function emailFinder (email: object) {
function emailNotificationFinder (email: object) {
const text = email['text']
return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
}

await checkNotification(base, notificationChecker, emailFinder, 'presence')
await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
}

// ---------------------------------------------------------------------------


+ 7
- 3
shared/models/server/emailer.model.ts View File

@@ -1,8 +1,12 @@
export type SendEmailOptions = {
to: string[]
subject: string
text: string

fromDisplayName?: string
template?: string
locals?: { [key: string]: any }

// override defaults
subject?: string
text?: string
from?: string | { name?: string, address: string }
replyTo?: string
}

+ 978
- 21
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save