diff --git a/client/src/app/+admin/config/pages/admin-config-customization.component.html b/client/src/app/+admin/config/pages/admin-config-customization.component.html index 9747e6f60..60af82671 100644 --- a/client/src/app/+admin/config/pages/admin-config-customization.component.html +++ b/client/src/app/+admin/config/pages/admin-config-customization.component.html @@ -111,11 +111,44 @@ +
+
+

EMAIL

+
+ + +
+
+ +
Support
{{ '{{instanceName}}' }}
template variable
+ + + + +
+ +
+ +
Support
{{ '{{instanceName}}' }}
template variable
+ + + + +
+
+
+
+
- -

Advanced

+

ADVANCED

Advanced modifications to your PeerTube platform if creating a plugin or a theme is overkill.
diff --git a/client/src/app/+admin/config/pages/admin-config-customization.component.ts b/client/src/app/+admin/config/pages/admin-config-customization.component.ts index 6e00a40f3..82f48cb1e 100644 --- a/client/src/app/+admin/config/pages/admin-config-customization.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-customization.component.ts @@ -38,6 +38,16 @@ type Form = { }> }> + email: FormGroup<{ + subject: FormGroup<{ + prefix: FormControl + }> + + body: FormGroup<{ + signature: FormControl + }> + }> + theme: FormGroup<{ default: FormControl @@ -197,6 +207,14 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can } } }, + email: { + subject: { + prefix: null + }, + body: { + signature: null + } + }, instance: { customizations: { css: null, diff --git a/config/default.yaml b/config/default.yaml index 1c8a70953..818c237ba 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -115,12 +115,6 @@ smtp: ca_file: null # Used for self signed certificates from_address: 'admin@example.com' -email: - body: - signature: 'PeerTube' - subject: - prefix: '[PeerTube]' - # From the project root directory storage: tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... @@ -1136,3 +1130,11 @@ defaults: player: # By default, playback starts automatically when opening a video auto_play: true + +email: + body: + # Support {{instanceName}} template variable + signature: '' + subject: + # Support {{instanceName}} template variable + prefix: '[{{instanceName}}] ' diff --git a/config/production.yaml.example b/config/production.yaml.example index c44019e15..7015b1146 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -113,12 +113,6 @@ smtp: ca_file: null # Used for self signed certificates from_address: 'admin@example.com' -email: - body: - signature: 'PeerTube' - subject: - prefix: '[PeerTube]' - # From the project root directory storage: tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... @@ -1146,3 +1140,11 @@ defaults: player: # By default, playback starts automatically when opening a video auto_play: true + +email: + body: + # Support {{instanceName}} template variable + signature: '' + subject: + # Support {{instanceName}} template variable + prefix: '[{{instanceName}}] ' diff --git a/packages/models/src/server/custom-config.model.ts b/packages/models/src/server/custom-config.model.ts index afdc6f164..7d4558d7a 100644 --- a/packages/models/src/server/custom-config.model.ts +++ b/packages/models/src/server/custom-config.model.ts @@ -122,6 +122,16 @@ export interface CustomConfig { email: string } + email: { + body: { + signature: string + } + + subject: { + prefix: string + } + } + contactForm: { enabled: boolean } diff --git a/packages/models/src/server/emailer.model.ts b/packages/models/src/server/emailer.model.ts index 39512d306..a69686780 100644 --- a/packages/models/src/server/emailer.model.ts +++ b/packages/models/src/server/emailer.model.ts @@ -26,6 +26,10 @@ interface SendEmailDefaultLocalsOptions { instanceName: string text: string subject: string + + fg: string + bg: string + primary: string } interface SendEmailDefaultMessageOptions { @@ -42,7 +46,7 @@ export type SendEmailDefaultOptions = { locals: SendEmailDefaultLocalsOptions & { WEBSERVER: any - EMAIL: any + signature: string } } diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts index 1b9c8940d..230fccad3 100644 --- a/packages/tests/src/api/server/config.ts +++ b/packages/tests/src/api/server/config.ts @@ -154,6 +154,9 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { expect(data.defaults.p2p.embed.enabled).to.be.true expect(data.defaults.p2p.webapp.enabled).to.be.true expect(data.defaults.player.autoPlay).to.be.true + + expect(data.email.body.signature).to.equal('') + expect(data.email.subject.prefix).to.equal('[{{instanceName}}] ') } function buildNewCustomConfig (server: PeerTubeServer): CustomConfig { @@ -449,6 +452,14 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig { player: { autoPlay: false } + }, + email: { + body: { + signature: 'my signature' + }, + subject: { + prefix: 'my prefix' + } } } } diff --git a/packages/tests/src/api/server/email.ts b/packages/tests/src/api/server/email.ts index bed208f72..4dec79707 100644 --- a/packages/tests/src/api/server/email.ts +++ b/packages/tests/src/api/server/email.ts @@ -9,6 +9,7 @@ import { setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' +import { expectStartWith } from '@tests/shared/checks.js' import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' import { SQLCommand } from '@tests/shared/sql-command.js' import { expect } from 'chai' @@ -79,7 +80,6 @@ describe('Test emails', function () { }) describe('When resetting user password', function () { - it('Should ask to reset the password', async function () { await server.users.askResetPassword({ email: 'user_1@example.com' }) @@ -140,7 +140,6 @@ describe('Test emails', function () { }) describe('When creating a user without password', function () { - it('Should send a create password email', async function () { await server.users.create({ username: 'create_password', password: '' }) @@ -193,7 +192,6 @@ describe('Test emails', function () { }) describe('When creating an abuse', function () { - it('Should send the notification email', async function () { const reason = 'my super bad reason' await server.abuses.report({ token: userAccessToken, videoId, reason }) @@ -212,7 +210,6 @@ describe('Test emails', function () { }) describe('When blocking/unblocking user', function () { - it('Should send the notification email when blocking a user', async function () { const reason = 'my super bad reason' await server.users.banUser({ userId, reason }) @@ -286,7 +283,6 @@ describe('Test emails', function () { }) describe('When verifying a user email', function () { - it('Should fail with wrong capitalization when multiple users with similar email exists', async function () { await server.users.askSendVerifyEmail({ email: similarUsers[0].username.toUpperCase(), @@ -388,6 +384,34 @@ describe('Test emails', function () { }) }) + describe('Email config', function () { + it('Should configure email subject prefix and body signature', async function () { + await server.config.updateExistingConfig({ + newConfig: { + instance: { + name: 'My tube' + }, + email: { + subject: { + prefix: 'My custom prefix {{instanceName}}' + }, + body: { + signature: 'My custom signature {{instanceName}}' + } + } + } + }) + + await server.users.banUser({ userId }) + await waitJobs(server) + + const email = emails[emails.length - 1] + + expectStartWith(email['subject'], 'My custom prefix My tube') + expect(email['text']).to.contain('My custom signature My tube') + }) + }) + after(async function () { MockSmtpServer.Instance.kill() diff --git a/server/core/assets/email-templates/common/base.pug b/server/core/assets/email-templates/common/base.pug index 41e94564d..8f15f2703 100644 --- a/server/core/assets/email-templates/common/base.pug +++ b/server/core/assets/email-templates/common/base.pug @@ -3,8 +3,6 @@ 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! @@ -74,15 +72,15 @@ head 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; + color: #{fg}; } - a:not(.nocolor) { - color: #{mainColor}; - } - a.nocolor { - color: inherit !important; + a:not(.no-color) { + font-weight: 600; + text-decoration: underline; + text-decoration-color: #{primary}; + text-underline-offset: 0.25em; + text-decoration-thickness: 0.15em; } /* What it does: A work-around for email clients meddling in triggered links. */ a[x-apple-data-detectors], /* iOS */ @@ -135,33 +133,13 @@ head style. blockquote { margin-left: 0; - padding-left: 20px; - border-left: 2px solid #f2690d; + padding-left: 10px; + border-left: 2px solid #{primary}; } //- 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};') +body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; color: #{fg}; background-color: #{bg};") + center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{bg};') //if mso | IE table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;') tr @@ -190,16 +168,16 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: 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;') + td 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;') + td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 1.5') 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="" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;") + img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="" border="0" style="height: 30px; font-family: sans-serif; font-size: 15px; line-height: 15px;") td - h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;') + h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; font-weight: normal;') block title if title | #{title} @@ -213,8 +191,8 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: //- 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} + td(style=`border-radius: 4px; background: ${primary};`) + a.no-color(href=action.url style=`background: ${primary}; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; font-weight: bold;`) #{action.text} //- Button : END //- 1 Column Text + Button : END //- Clear Spacer : BEGIN @@ -227,32 +205,15 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: unless hideNotificationPreferencesLink 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;') + td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; text-align: center;') webversion - a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications + a.no-color(href=`${WEBSERVER.URL}/my-account/notifications` style='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;') + td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; text-align: center;') unsubscribe - a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile + a.no-color(href=`${WEBSERVER.URL}/my-account/settings#notifications`) 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 diff --git a/server/core/assets/email-templates/common/greetings.pug b/server/core/assets/email-templates/common/greetings.pug index 5efe29dfb..2170d3814 100644 --- a/server/core/assets/email-templates/common/greetings.pug +++ b/server/core/assets/email-templates/common/greetings.pug @@ -3,9 +3,9 @@ extends base block body if username p Hi #{username}, - else - p Hi, + block content - p - | Cheers,#[br] - | #{EMAIL.BODY.SIGNATURE} \ No newline at end of file + + if signature + p + | #{signature} diff --git a/server/core/controllers/api/config.ts b/server/core/controllers/api/config.ts index 309c67257..6b23e1164 100644 --- a/server/core/controllers/api/config.ts +++ b/server/core/controllers/api/config.ts @@ -528,6 +528,15 @@ function customConfig (): CustomConfig { player: { autoPlay: CONFIG.DEFAULTS.PLAYER.AUTO_PLAY } + }, + + email: { + body: { + signature: CONFIG.EMAIL.BODY.SIGNATURE + }, + subject: { + prefix: CONFIG.EMAIL.SUBJECT.PREFIX + } } } } diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts index 44feaf891..310c7eb6f 100644 --- a/server/core/initializers/config.ts +++ b/server/core/initializers/config.ts @@ -65,14 +65,6 @@ const CONFIG = { CA_FILE: config.get('smtp.ca_file'), FROM_ADDRESS: config.get('smtp.from_address') }, - EMAIL: { - BODY: { - SIGNATURE: config.get('email.body.signature') - }, - SUBJECT: { - PREFIX: config.get('email.subject.prefix') + ' ' - } - }, NSFW_FLAGS_SETTINGS: { ENABLED: config.get('nsfw_flags_settings.enabled') @@ -1081,6 +1073,18 @@ const CONFIG = { get ENABLED () { return config.get('storyboards.enabled') } + }, + EMAIL: { + BODY: { + get SIGNATURE () { + return config.get('email.body.signature') + } + }, + SUBJECT: { + get PREFIX () { + return config.get('email.subject.prefix') + } + } } } diff --git a/server/core/lib/emailer.ts b/server/core/lib/emailer.ts index de1424a74..11411119d 100644 --- a/server/core/lib/emailer.ts +++ b/server/core/lib/emailer.ts @@ -14,7 +14,7 @@ import { MRegistration, MUser, MUserExport, MUserImport } from '../types/models/ import { JobQueue } from './job-queue/index.js' import { Hooks } from './plugins/hooks.js' -class Emailer { +export class Emailer { private static instance: Emailer private initialized = false private transporter: Transporter @@ -299,7 +299,7 @@ class Emailer { from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>` }, transport: this.transporter, - subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX + subjectPrefix: this.buildSubjectPrefix() }) const subject = await Hooks.wrapObject( options.subject, @@ -322,10 +322,13 @@ class Emailer { }, locals: { // default variables available in all templates WEBSERVER, - EMAIL: CONFIG.EMAIL, instanceName: CONFIG.INSTANCE.NAME, text: options.text, - subject + subject, + signature: this.buildSignature(), + fg: CONFIG.THEME.CUSTOMIZATION.FOREGROUND_COLOR || '#000', + bg: CONFIG.THEME.CUSTOMIZATION.BACKGROUND_COLOR || '#fff', + primary: CONFIG.THEME.CUSTOMIZATION.PRIMARY_COLOR || '#FF8F37' } } @@ -396,13 +399,24 @@ class Emailer { }) } + private buildSubjectPrefix () { + let prefix = CONFIG.EMAIL.SUBJECT.PREFIX + if (!prefix) return prefix + + prefix = prefix.replace(/{{instanceName}}/g, CONFIG.INSTANCE.NAME) + if (prefix.endsWith(' ')) return prefix + + return prefix + ' ' + } + + private buildSignature () { + const signature = CONFIG.EMAIL.BODY.SIGNATURE + if (!signature) return signature + + return signature.replace(/{{instanceName}}/g, CONFIG.INSTANCE.NAME) + } + static get Instance () { return this.instance || (this.instance = new this()) } } - -// --------------------------------------------------------------------------- - -export { - Emailer -} diff --git a/server/core/middlewares/validators/config.ts b/server/core/middlewares/validators/config.ts index 52dc6f6e4..93d6939f1 100644 --- a/server/core/middlewares/validators/config.ts +++ b/server/core/middlewares/validators/config.ts @@ -143,6 +143,9 @@ const customConfigUpdateValidator = [ body('defaults.p2p.embed.enabled').isBoolean(), body('defaults.player.autoPlay').isBoolean(), + body('email.body.signature').exists(), + body('email.subject.prefix').exists(), + (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return