mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Scheduled change conflict email templates and function (#5547)
Creates a new email template for scheduled change conflicts and a function to send it. Relates to: #[1-1686](https://linear.app/unleash/issue/1-1686/send-an-email-when-the-conflicts-are-detected)  --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
		
							parent
							
								
									da1a9d4036
								
							
						
					
					
						commit
						12f79f90bb
					
				@ -1,10 +1,11 @@
 | 
				
			|||||||
import nodemailer from 'nodemailer';
 | 
					import nodemailer from 'nodemailer';
 | 
				
			||||||
import { EmailService } from './email-service';
 | 
					import { EmailService } from './email-service';
 | 
				
			||||||
import noLoggerProvider from '../../test/fixtures/no-logger';
 | 
					import noLoggerProvider from '../../test/fixtures/no-logger';
 | 
				
			||||||
 | 
					import { IUnleashConfig } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Can send reset email', async () => {
 | 
					test('Can send reset email', async () => {
 | 
				
			||||||
    const emailService = new EmailService(
 | 
					    const emailService = new EmailService({
 | 
				
			||||||
        {
 | 
					        email: {
 | 
				
			||||||
            host: 'test',
 | 
					            host: 'test',
 | 
				
			||||||
            port: 587,
 | 
					            port: 587,
 | 
				
			||||||
            secure: false,
 | 
					            secure: false,
 | 
				
			||||||
@ -12,8 +13,8 @@ test('Can send reset email', async () => {
 | 
				
			|||||||
            smtppass: '',
 | 
					            smtppass: '',
 | 
				
			||||||
            sender: 'noreply@getunleash.ai',
 | 
					            sender: 'noreply@getunleash.ai',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        noLoggerProvider,
 | 
					        getLogger: noLoggerProvider,
 | 
				
			||||||
    );
 | 
					    } as unknown as IUnleashConfig);
 | 
				
			||||||
    const resetLinkUrl =
 | 
					    const resetLinkUrl =
 | 
				
			||||||
        'https://unleash-hosted.com/reset-password?token=$2b$10$M06Ysso6KL4ueH/xR6rdSuY5GSymdIwmIkEUJMRkB.Qn26r5Gi5vW';
 | 
					        'https://unleash-hosted.com/reset-password?token=$2b$10$M06Ysso6KL4ueH/xR6rdSuY5GSymdIwmIkEUJMRkB.Qn26r5Gi5vW';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,17 +30,17 @@ test('Can send reset email', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Can send welcome mail', async () => {
 | 
					test('Can send welcome mail', async () => {
 | 
				
			||||||
    const emailService = new EmailService(
 | 
					    const emailService = new EmailService({
 | 
				
			||||||
        {
 | 
					        email: {
 | 
				
			||||||
            host: 'test',
 | 
					            host: 'test',
 | 
				
			||||||
            port: 9999,
 | 
					            port: 587,
 | 
				
			||||||
            secure: false,
 | 
					            secure: false,
 | 
				
			||||||
            sender: 'noreply@getunleash.ai',
 | 
					 | 
				
			||||||
            smtpuser: '',
 | 
					            smtpuser: '',
 | 
				
			||||||
            smtppass: '',
 | 
					            smtppass: '',
 | 
				
			||||||
 | 
					            sender: 'noreply@getunleash.ai',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        noLoggerProvider,
 | 
					        getLogger: noLoggerProvider,
 | 
				
			||||||
    );
 | 
					    } as unknown as IUnleashConfig);
 | 
				
			||||||
    const content = await emailService.sendGettingStartedMail(
 | 
					    const content = await emailService.sendGettingStartedMail(
 | 
				
			||||||
        'Some username',
 | 
					        'Some username',
 | 
				
			||||||
        'test@test.com',
 | 
					        'test@test.com',
 | 
				
			||||||
@ -52,8 +53,8 @@ test('Can send welcome mail', async () => {
 | 
				
			|||||||
test('Can supply additional SMTP transport options', async () => {
 | 
					test('Can supply additional SMTP transport options', async () => {
 | 
				
			||||||
    const spy = jest.spyOn(nodemailer, 'createTransport');
 | 
					    const spy = jest.spyOn(nodemailer, 'createTransport');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    new EmailService(
 | 
					    new EmailService({
 | 
				
			||||||
        {
 | 
					        email: {
 | 
				
			||||||
            host: 'smtp.unleash.test',
 | 
					            host: 'smtp.unleash.test',
 | 
				
			||||||
            port: 9999,
 | 
					            port: 9999,
 | 
				
			||||||
            secure: false,
 | 
					            secure: false,
 | 
				
			||||||
@ -64,8 +65,8 @@ test('Can supply additional SMTP transport options', async () => {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        noLoggerProvider,
 | 
					        getLogger: noLoggerProvider,
 | 
				
			||||||
    );
 | 
					    } as unknown as IUnleashConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(spy).toHaveBeenCalledWith({
 | 
					    expect(spy).toHaveBeenCalledWith({
 | 
				
			||||||
        auth: {
 | 
					        auth: {
 | 
				
			||||||
@ -82,17 +83,17 @@ test('Can supply additional SMTP transport options', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should strip special characters from email subject', async () => {
 | 
					test('should strip special characters from email subject', async () => {
 | 
				
			||||||
    const emailService = new EmailService(
 | 
					    const emailService = new EmailService({
 | 
				
			||||||
        {
 | 
					        email: {
 | 
				
			||||||
            host: 'test',
 | 
					            host: 'test',
 | 
				
			||||||
            port: 9999,
 | 
					            port: 587,
 | 
				
			||||||
            secure: false,
 | 
					            secure: false,
 | 
				
			||||||
            sender: 'noreply@getunleash.ai',
 | 
					 | 
				
			||||||
            smtpuser: '',
 | 
					            smtpuser: '',
 | 
				
			||||||
            smtppass: '',
 | 
					            smtppass: '',
 | 
				
			||||||
 | 
					            sender: 'noreply@getunleash.ai',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        noLoggerProvider,
 | 
					        getLogger: noLoggerProvider,
 | 
				
			||||||
    );
 | 
					    } as unknown as IUnleashConfig);
 | 
				
			||||||
    expect(emailService.stripSpecialCharacters('http://evil.com')).toBe(
 | 
					    expect(emailService.stripSpecialCharacters('http://evil.com')).toBe(
 | 
				
			||||||
        'httpevilcom',
 | 
					        'httpevilcom',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import { createTransport, Transporter } from 'nodemailer';
 | 
					import { createTransport, Transporter } from 'nodemailer';
 | 
				
			||||||
import Mustache from 'mustache';
 | 
					import Mustache from 'mustache';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import { readFileSync, existsSync } from 'fs';
 | 
					import { existsSync, readFileSync } from 'fs';
 | 
				
			||||||
import { Logger, LogProvider } from '../logger';
 | 
					import { Logger } from '../logger';
 | 
				
			||||||
import NotFoundError from '../error/notfound-error';
 | 
					import NotFoundError from '../error/notfound-error';
 | 
				
			||||||
import { IEmailOption } from '../types/option';
 | 
					import { IUnleashConfig } from '../types/option';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAuthOptions {
 | 
					export interface IAuthOptions {
 | 
				
			||||||
    user: string;
 | 
					    user: string;
 | 
				
			||||||
@ -31,6 +31,8 @@ export interface IEmailEnvelope {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const RESET_MAIL_SUBJECT = 'Unleash - Reset your password';
 | 
					const RESET_MAIL_SUBJECT = 'Unleash - Reset your password';
 | 
				
			||||||
const GETTING_STARTED_SUBJECT = 'Welcome to Unleash';
 | 
					const GETTING_STARTED_SUBJECT = 'Welcome to Unleash';
 | 
				
			||||||
 | 
					const SCHEDULED_CHANGE_CONFLICT_SUBJECT =
 | 
				
			||||||
 | 
					    'Unleash - Scheduled changes can no longer be applied';
 | 
				
			||||||
const SCHEDULED_EXECUTION_FAILED_SUBJECT =
 | 
					const SCHEDULED_EXECUTION_FAILED_SUBJECT =
 | 
				
			||||||
    'Unleash - Scheduled change request could not be applied';
 | 
					    'Unleash - Scheduled change request could not be applied';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,13 +40,16 @@ export const MAIL_ACCEPTED = '250 Accepted';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export class EmailService {
 | 
					export class EmailService {
 | 
				
			||||||
    private logger: Logger;
 | 
					    private logger: Logger;
 | 
				
			||||||
 | 
					    private config: IUnleashConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly mailer?: Transporter;
 | 
					    private readonly mailer?: Transporter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly sender: string;
 | 
					    private readonly sender: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(email: IEmailOption | undefined, getLogger: LogProvider) {
 | 
					    constructor(config: IUnleashConfig) {
 | 
				
			||||||
        this.logger = getLogger('services/email-service.ts');
 | 
					        this.config = config;
 | 
				
			||||||
 | 
					        this.logger = config.getLogger('services/email-service.ts');
 | 
				
			||||||
 | 
					        const { email } = config;
 | 
				
			||||||
        if (email?.host) {
 | 
					        if (email?.host) {
 | 
				
			||||||
            this.sender = email.sender;
 | 
					            this.sender = email.sender;
 | 
				
			||||||
            if (email.host === 'test') {
 | 
					            if (email.host === 'test') {
 | 
				
			||||||
@ -138,6 +143,95 @@ export class EmailService {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async sendScheduledChangeConflictEmail(
 | 
				
			||||||
 | 
					        recipient: string,
 | 
				
			||||||
 | 
					        conflictScope: 'flag' | 'strategy',
 | 
				
			||||||
 | 
					        changeRequests: {
 | 
				
			||||||
 | 
					            id: number;
 | 
				
			||||||
 | 
					            scheduledAt: string;
 | 
				
			||||||
 | 
					            link: string;
 | 
				
			||||||
 | 
					            title?: string;
 | 
				
			||||||
 | 
					        }[],
 | 
				
			||||||
 | 
					        flagName: string,
 | 
				
			||||||
 | 
					        project: string,
 | 
				
			||||||
 | 
					        strategyId?: string,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        if (this.configured()) {
 | 
				
			||||||
 | 
					            const year = new Date().getFullYear();
 | 
				
			||||||
 | 
					            const conflict =
 | 
				
			||||||
 | 
					                conflictScope === 'flag'
 | 
				
			||||||
 | 
					                    ? `The feature flag ${flagName} in ${project} has been archived`
 | 
				
			||||||
 | 
					                    : `The strategy with id ${strategyId} for flag ${flagName} in ${project} has been deleted`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const conflictResolution =
 | 
				
			||||||
 | 
					                conflictScope === 'flag'
 | 
				
			||||||
 | 
					                    ? ' unless the flag is revived'
 | 
				
			||||||
 | 
					                    : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const conflictResolutionLink = conflictResolution
 | 
				
			||||||
 | 
					                ? `${this.config.server.baseUriPath}/projects/${project}/archive?sort=archivedAt&search=${flagName}`
 | 
				
			||||||
 | 
					                : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const bodyHtml = await this.compileTemplate(
 | 
				
			||||||
 | 
					                'scheduled-change-conflict',
 | 
				
			||||||
 | 
					                TemplateFormat.HTML,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    conflict,
 | 
				
			||||||
 | 
					                    conflictScope,
 | 
				
			||||||
 | 
					                    conflictResolution,
 | 
				
			||||||
 | 
					                    conflictResolutionLink,
 | 
				
			||||||
 | 
					                    changeRequests,
 | 
				
			||||||
 | 
					                    year,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const bodyText = await this.compileTemplate(
 | 
				
			||||||
 | 
					                'scheduled-change-conflict',
 | 
				
			||||||
 | 
					                TemplateFormat.PLAIN,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    conflict,
 | 
				
			||||||
 | 
					                    conflictScope,
 | 
				
			||||||
 | 
					                    conflictResolution,
 | 
				
			||||||
 | 
					                    conflictResolutionLink,
 | 
				
			||||||
 | 
					                    changeRequests,
 | 
				
			||||||
 | 
					                    year,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const email = {
 | 
				
			||||||
 | 
					                from: this.sender,
 | 
				
			||||||
 | 
					                to: recipient,
 | 
				
			||||||
 | 
					                subject: SCHEDULED_CHANGE_CONFLICT_SUBJECT,
 | 
				
			||||||
 | 
					                html: bodyHtml,
 | 
				
			||||||
 | 
					                text: bodyText,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            process.nextTick(() => {
 | 
				
			||||||
 | 
					                this.mailer!.sendMail(email).then(
 | 
				
			||||||
 | 
					                    () =>
 | 
				
			||||||
 | 
					                        this.logger.info(
 | 
				
			||||||
 | 
					                            'Successfully sent scheduled-change-conflict email',
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                    (e) =>
 | 
				
			||||||
 | 
					                        this.logger.warn(
 | 
				
			||||||
 | 
					                            'Failed to send scheduled-change-conflict email',
 | 
				
			||||||
 | 
					                            e,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return Promise.resolve(email);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return new Promise((res) => {
 | 
				
			||||||
 | 
					            this.logger.warn(
 | 
				
			||||||
 | 
					                'No mailer is configured. Please read the docs on how to configure an email service',
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            res({
 | 
				
			||||||
 | 
					                from: this.sender,
 | 
				
			||||||
 | 
					                to: recipient,
 | 
				
			||||||
 | 
					                subject: SCHEDULED_CHANGE_CONFLICT_SUBJECT,
 | 
				
			||||||
 | 
					                html: '',
 | 
				
			||||||
 | 
					                text: '',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async sendResetMail(
 | 
					    async sendResetMail(
 | 
				
			||||||
        name: string,
 | 
					        name: string,
 | 
				
			||||||
        recipient: string,
 | 
					        recipient: string,
 | 
				
			||||||
 | 
				
			|||||||
@ -140,7 +140,7 @@ export const createServices = (
 | 
				
			|||||||
        eventService,
 | 
					        eventService,
 | 
				
			||||||
        privateProjectChecker,
 | 
					        privateProjectChecker,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const featureTypeService = new FeatureTypeService(
 | 
					    const featureTypeService = new FeatureTypeService(
 | 
				
			||||||
        stores,
 | 
					        stores,
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ test('Should create new user', async () => {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
        { eventStore, featureTagStore: new FakeFeatureTagStore() },
 | 
					        { eventStore, featureTagStore: new FakeFeatureTagStore() },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
@ -78,7 +78,7 @@ test('Should create default user - with defaults', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -117,7 +117,7 @@ test('Should create default user - with provided username and password', async (
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -161,7 +161,7 @@ test('Should not create default user - with `createAdminUser` === false', async
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -210,7 +210,7 @@ test('Should be a valid password', async () => {
 | 
				
			|||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -248,7 +248,7 @@ test('Password must be at least 10 chars', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -288,7 +288,7 @@ test('The password must contain at least one uppercase letter.', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -330,7 +330,7 @@ test('The password must contain at least one number', async () => {
 | 
				
			|||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -371,7 +371,7 @@ test('The password must contain at least one special character', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -412,7 +412,7 @@ test('Should be a valid password with special chars', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -450,7 +450,7 @@ test('Should send password reset email if user exists', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
@ -504,7 +504,7 @@ test('Should throttle password reset email', async () => {
 | 
				
			|||||||
        { resetTokenStore },
 | 
					        { resetTokenStore },
 | 
				
			||||||
        config,
 | 
					        config,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new FakeSessionStore();
 | 
					    const sessionStore = new FakeSessionStore();
 | 
				
			||||||
    const sessionService = new SessionService({ sessionStore }, config);
 | 
					    const sessionService = new SessionService({ sessionStore }, config);
 | 
				
			||||||
    const eventService = new EventService(
 | 
					    const eventService = new EventService(
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,527 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 | 
				
			||||||
 | 
					<html xmlns="http://www.w3.org/1999/xhtml">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 | 
				
			||||||
 | 
					    <title>*|MC:SUBJECT|*</title>
 | 
				
			||||||
 | 
					    <style type="text/css">
 | 
				
			||||||
 | 
					        /* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					        #outlook a{padding:0;} /* Force Outlook to provide a "view in browser" message */
 | 
				
			||||||
 | 
					        .ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
 | 
				
			||||||
 | 
					        .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} /* Force Hotmail to display normal line spacing */
 | 
				
			||||||
 | 
					        body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;} /* Prevent WebKit and Windows mobile changing default text sizes */
 | 
				
			||||||
 | 
					        table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;} /* Remove spacing between tables in Outlook 2007 and up */
 | 
				
			||||||
 | 
					        img{-ms-interpolation-mode:bicubic;} /* Allow smoother rendering of resized image in Internet Explorer */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					        body{margin:0; padding:0;}
 | 
				
			||||||
 | 
					        img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
 | 
				
			||||||
 | 
					        table{border-collapse:collapse !important;}
 | 
				
			||||||
 | 
					        body, #bodyTable, #bodyCell{height:100% !important; margin:0; padding:0; width:100% !important;}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* ========== Page Styles ========== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #bodyCell{padding:20px;}
 | 
				
			||||||
 | 
					        #templateContainer{width:600px;}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section background style
 | 
				
			||||||
 | 
					        * @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding.
 | 
				
			||||||
 | 
					        * @theme page
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        body, #bodyTable{
 | 
				
			||||||
 | 
					            /*@editable*/ background-color:#DEE0E2;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section background style
 | 
				
			||||||
 | 
					        * @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding.
 | 
				
			||||||
 | 
					        * @theme page
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #bodyCell{
 | 
				
			||||||
 | 
					            /*@editable*/ border-top:4px solid #BBBBBB;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section email border
 | 
				
			||||||
 | 
					        * @tip Set the border for your email.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #templateContainer{
 | 
				
			||||||
 | 
					            /*@editable*/ border:1px solid #BBBBBB;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section heading 1
 | 
				
			||||||
 | 
					        * @tip Set the styling for all first-level headings in your emails. These should be the largest of your headings.
 | 
				
			||||||
 | 
					        * @style heading 1
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        h1{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#202020 !important;
 | 
				
			||||||
 | 
					            display:block;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:26px;
 | 
				
			||||||
 | 
					            /*@editable*/ font-style:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:bold;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:100%;
 | 
				
			||||||
 | 
					            /*@editable*/ letter-spacing:normal;
 | 
				
			||||||
 | 
					            margin-top:0;
 | 
				
			||||||
 | 
					            margin-right:0;
 | 
				
			||||||
 | 
					            margin-bottom:10px;
 | 
				
			||||||
 | 
					            margin-left:0;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section heading 2
 | 
				
			||||||
 | 
					        * @tip Set the styling for all second-level headings in your emails.
 | 
				
			||||||
 | 
					        * @style heading 2
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        h2{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#404040 !important;
 | 
				
			||||||
 | 
					            display:block;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:20px;
 | 
				
			||||||
 | 
					            /*@editable*/ font-style:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:bold;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:100%;
 | 
				
			||||||
 | 
					            /*@editable*/ letter-spacing:normal;
 | 
				
			||||||
 | 
					            margin-top:0;
 | 
				
			||||||
 | 
					            margin-right:0;
 | 
				
			||||||
 | 
					            margin-bottom:10px;
 | 
				
			||||||
 | 
					            margin-left:0;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section heading 3
 | 
				
			||||||
 | 
					        * @tip Set the styling for all third-level headings in your emails.
 | 
				
			||||||
 | 
					        * @style heading 3
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        h3{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#606060 !important;
 | 
				
			||||||
 | 
					            display:block;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:16px;
 | 
				
			||||||
 | 
					            /*@editable*/ font-style:italic;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:100%;
 | 
				
			||||||
 | 
					            /*@editable*/ letter-spacing:normal;
 | 
				
			||||||
 | 
					            margin-top:0;
 | 
				
			||||||
 | 
					            margin-right:0;
 | 
				
			||||||
 | 
					            margin-bottom:10px;
 | 
				
			||||||
 | 
					            margin-left:0;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Page
 | 
				
			||||||
 | 
					        * @section heading 4
 | 
				
			||||||
 | 
					        * @tip Set the styling for all fourth-level headings in your emails. These should be the smallest of your headings.
 | 
				
			||||||
 | 
					        * @style heading 4
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        h4{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#808080 !important;
 | 
				
			||||||
 | 
					            display:block;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:14px;
 | 
				
			||||||
 | 
					            /*@editable*/ font-style:italic;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:100%;
 | 
				
			||||||
 | 
					            /*@editable*/ letter-spacing:normal;
 | 
				
			||||||
 | 
					            margin-top:0;
 | 
				
			||||||
 | 
					            margin-right:0;
 | 
				
			||||||
 | 
					            margin-bottom:10px;
 | 
				
			||||||
 | 
					            margin-left:0;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* ========== Header Styles ========== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section preheader style
 | 
				
			||||||
 | 
					        * @tip Set the background color and bottom border for your email's preheader area.
 | 
				
			||||||
 | 
					        * @theme header
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #templatePreheader{
 | 
				
			||||||
 | 
					            /*@editable*/ background-color:#fff;
 | 
				
			||||||
 | 
					            /*@editable*/ border-bottom:1px solid #CCCCCC;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section preheader text
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's preheader text. Choose a size and color that is easy to read.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .preheaderContent{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#808080;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:10px;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:125%;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section preheader link
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's preheader links. Choose a color that helps them stand out from your text.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .preheaderContent a:link, .preheaderContent a:visited, /* Yahoo! Mail Override */ .preheaderContent a .yshortcuts /* Yahoo! Mail Override */{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#fff;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ text-decoration:underline;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section header style
 | 
				
			||||||
 | 
					        * @tip Set the background color and borders for your email's header area.
 | 
				
			||||||
 | 
					        * @theme header
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #templateHeader{
 | 
				
			||||||
 | 
					            /*@editable*/ background-color:#fff;
 | 
				
			||||||
 | 
					            /*@editable*/ border-top:1px solid #FFFFFF;
 | 
				
			||||||
 | 
					            /*@editable*/ border-bottom:1px solid #CCCCCC;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section header text
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's header text. Choose a size and color that is easy to read.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .headerContent{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#505050;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:20px;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:bold;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:100%;
 | 
				
			||||||
 | 
					            /*@editable*/ padding-top:0;
 | 
				
			||||||
 | 
					            /*@editable*/ padding-right:0;
 | 
				
			||||||
 | 
					            /*@editable*/ padding-bottom:0;
 | 
				
			||||||
 | 
					            /*@editable*/ padding-left:0;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					            /*@editable*/ vertical-align:middle;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Header
 | 
				
			||||||
 | 
					        * @section header link
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's header links. Choose a color that helps them stand out from your text.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .headerContent a:link, .headerContent a:visited, /* Yahoo! Mail Override */ .headerContent a .yshortcuts /* Yahoo! Mail Override */{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#fff;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ text-decoration:underline;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #headerImage{
 | 
				
			||||||
 | 
					            height:auto;
 | 
				
			||||||
 | 
					            max-width:600px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* ========== Body Styles ========== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Body
 | 
				
			||||||
 | 
					        * @section body style
 | 
				
			||||||
 | 
					        * @tip Set the background color and borders for your email's body area.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #templateBody{
 | 
				
			||||||
 | 
					            /*@editable*/ background-color:#fff;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Body
 | 
				
			||||||
 | 
					        * @section body text
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's main content text. Choose a size and color that is easy to read.
 | 
				
			||||||
 | 
					        * @theme main
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .bodyContent{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#505050;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:14px;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:150%;
 | 
				
			||||||
 | 
					            /*@editable*/ border-bottom: 1px solid #CCCCCC;
 | 
				
			||||||
 | 
					            padding-top:20px;
 | 
				
			||||||
 | 
					            padding-right:20px;
 | 
				
			||||||
 | 
					            padding-bottom:20px;
 | 
				
			||||||
 | 
					            padding-left:20px;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .bodyContent img{
 | 
				
			||||||
 | 
					            display:inline;
 | 
				
			||||||
 | 
					            height:auto;
 | 
				
			||||||
 | 
					            max-width:560px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .changeRequestLink {
 | 
				
			||||||
 | 
					            text-decoration: none;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .changeRequestLink:hover {
 | 
				
			||||||
 | 
					            text-decoration: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* ========== Footer Styles ========== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Footer
 | 
				
			||||||
 | 
					        * @section footer style
 | 
				
			||||||
 | 
					        * @tip Set the background color and borders for your email's footer area.
 | 
				
			||||||
 | 
					        * @theme footer
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        #templateFooter{
 | 
				
			||||||
 | 
					            /*@editable*/ background-color:#fff;
 | 
				
			||||||
 | 
					            /*@editable*/ border-top:1px solid #FFFFFF;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Footer
 | 
				
			||||||
 | 
					        * @section footer text
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's footer text. Choose a size and color that is easy to read.
 | 
				
			||||||
 | 
					        * @theme footer
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .footerContent{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#808080;
 | 
				
			||||||
 | 
					            /*@editable*/ font-family:Helvetica;
 | 
				
			||||||
 | 
					            /*@editable*/ font-size:10px;
 | 
				
			||||||
 | 
					            /*@editable*/ line-height:150%;
 | 
				
			||||||
 | 
					            padding-top:20px;
 | 
				
			||||||
 | 
					            padding-right:20px;
 | 
				
			||||||
 | 
					            padding-bottom:20px;
 | 
				
			||||||
 | 
					            padding-left:20px;
 | 
				
			||||||
 | 
					            /*@editable*/ text-align:left;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					        * @tab Footer
 | 
				
			||||||
 | 
					        * @section footer link
 | 
				
			||||||
 | 
					        * @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text.
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        .footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */{
 | 
				
			||||||
 | 
					            /*@editable*/ color:#606060;
 | 
				
			||||||
 | 
					            /*@editable*/ font-weight:normal;
 | 
				
			||||||
 | 
					            /*@editable*/ text-decoration:underline;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @media only screen and (max-width: 480px){
 | 
				
			||||||
 | 
					            /* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					            body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:none !important;} /* Prevent Webkit platforms from changing default text sizes */
 | 
				
			||||||
 | 
					            body{width:100% !important; min-width:100% !important;} /* Prevent iOS Mail from adding padding to the body */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					            #bodyCell{padding:10px !important;}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* ======== Page Styles ======== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section template width
 | 
				
			||||||
 | 
					            * @tip Make the template fluid for portrait or landscape view adaptability. If a fluid layout doesn't work for you, set the width to 300px instead.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            #templateContainer{
 | 
				
			||||||
 | 
					                max-width:600px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ width:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section heading 1
 | 
				
			||||||
 | 
					            * @tip Make the first-level headings larger in size for better readability on small screens.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            h1{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:24px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section heading 2
 | 
				
			||||||
 | 
					            * @tip Make the second-level headings larger in size for better readability on small screens.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            h2{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:20px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section heading 3
 | 
				
			||||||
 | 
					            * @tip Make the third-level headings larger in size for better readability on small screens.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            h3{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:18px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section heading 4
 | 
				
			||||||
 | 
					            * @tip Make the fourth-level headings larger in size for better readability on small screens.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            h4{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:16px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* ======== Header Styles ======== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            #templatePreheader{display:none !important;} /* Hide the template preheader to save space */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section header image
 | 
				
			||||||
 | 
					            * @tip Make the main header image fluid for portrait or landscape view adaptability, and set the image's original width as the max-width. If a fluid setting doesn't work, set the image width to half its original size instead.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            #headerImage{
 | 
				
			||||||
 | 
					                height:auto !important;
 | 
				
			||||||
 | 
					                /*@editable*/ max-width:600px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ width:100% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section header text
 | 
				
			||||||
 | 
					            * @tip Make the header content text larger in size for better readability on small screens. We recommend a font size of at least 16px.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            .headerContent{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:20px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:125% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* ======== Body Styles ======== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section body text
 | 
				
			||||||
 | 
					            * @tip Make the body content text larger in size for better readability on small screens. We recommend a font size of at least 16px.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            .bodyContent{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:18px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:125% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* ======== Footer Styles ======== */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					            * @tab Mobile Styles
 | 
				
			||||||
 | 
					            * @section footer text
 | 
				
			||||||
 | 
					            * @tip Make the body content text larger in size for better readability on small screens.
 | 
				
			||||||
 | 
					            */
 | 
				
			||||||
 | 
					            .footerContent{
 | 
				
			||||||
 | 
					                /*@editable*/ font-size:14px !important;
 | 
				
			||||||
 | 
					                /*@editable*/ line-height:115% !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .footerContent a{display:block !important;} /* Place footer social and utility links on their own lines, for easier access */
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
 | 
				
			||||||
 | 
					<center>
 | 
				
			||||||
 | 
					    <table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center" valign="top" id="bodyCell">
 | 
				
			||||||
 | 
					                <!-- BEGIN TEMPLATE // -->
 | 
				
			||||||
 | 
					                <table border="0" cellpadding="0" cellspacing="0" id="templateContainer">
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td align="center" valign="top">
 | 
				
			||||||
 | 
					                            <!-- BEGIN PREHEADER // -->
 | 
				
			||||||
 | 
					                            <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templatePreheader">
 | 
				
			||||||
 | 
					                                <tr>
 | 
				
			||||||
 | 
					                                    <td valign="top" class="preheaderContent" style="padding-top:10px; padding-right:20px; padding-bottom:10px; padding-left:20px;" mc:edit="preheader_content00">
 | 
				
			||||||
 | 
					                                        Scheduled changes can no longer be applied
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                    <!-- *|IFNOT:ARCHIVE_PAGE|* -->
 | 
				
			||||||
 | 
					                                    <td valign="top" width="180" class="preheaderContent" style="padding-top:10px; padding-right:20px; padding-bottom:10px; padding-left:0;" mc:edit="preheader_content01">
 | 
				
			||||||
 | 
					                                        Email not displaying correctly?<br /><a href="*|ARCHIVE|*" target="_blank">View it in your browser</a>.
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                    <!-- *|END:IF|* -->
 | 
				
			||||||
 | 
					                                </tr>
 | 
				
			||||||
 | 
					                            </table>
 | 
				
			||||||
 | 
					                            <!-- // END PREHEADER -->
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td align="center" valign="top">
 | 
				
			||||||
 | 
					                            <!-- BEGIN HEADER // -->
 | 
				
			||||||
 | 
					                            <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader">
 | 
				
			||||||
 | 
					                                <tr>
 | 
				
			||||||
 | 
					                                    <td valign="top" class="headerContent">
 | 
				
			||||||
 | 
					                                        <img src="https://cdn.getunleash.io/unleash_logo_600.png" style="max-width:600px;padding:1rem;" id="headerImage" mc:label="header_image" mc:edit="header_image" mc:allowdesigner mc:allowtext />
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                </tr>
 | 
				
			||||||
 | 
					                            </table>
 | 
				
			||||||
 | 
					                            <!-- // END HEADER -->
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td align="center" valign="top">
 | 
				
			||||||
 | 
					                            <!-- BEGIN BODY // -->
 | 
				
			||||||
 | 
					                            <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody">
 | 
				
			||||||
 | 
					                                <tr>
 | 
				
			||||||
 | 
					                                    <td valign="top" class="bodyContent" mc:edit="body_content">
 | 
				
			||||||
 | 
					                                        <h1>Conflict detected in a scheduled change</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					                                        {{ conflict }}. Scheduled change requests that use this {{ conflictScope }} can no longer be applied and their scheduled applications will fail{{#conflictResolution}}<span><a class="changeRequestLink" href="{{{ conflictResolutionLink }}}" target="_blank" rel="noopener noreferrer">{{.}}</a></span>{{/conflictResolution}}{{^conflictResolution}}.{{/conflictResolution}}
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					                                        For you, this concerns change requests:
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					                                        {{#changeRequests}}
 | 
				
			||||||
 | 
					                                        <ul>
 | 
				
			||||||
 | 
					                                            <li><span><a class="changeRequestLink" href="{{{ link }}}" target="_blank" rel="noopener noreferrer">#{{id}} {{#title}}- {{.}}{{/title}} (scheduled for {{scheduledAt}})</a></span></li>
 | 
				
			||||||
 | 
					                                        </ul>
 | 
				
			||||||
 | 
					                                        {{/changeRequests}}
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                </tr>
 | 
				
			||||||
 | 
					                            </table>
 | 
				
			||||||
 | 
					                            <!-- // END BODY -->
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td align="center" valign="top">
 | 
				
			||||||
 | 
					                            <!-- BEGIN FOOTER // -->
 | 
				
			||||||
 | 
					                            <table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter">
 | 
				
			||||||
 | 
					                                <tr>
 | 
				
			||||||
 | 
					                                    <td valign="top" class="footerContent" mc:edit="footer_content00">
 | 
				
			||||||
 | 
					                                        <a href="https://github.com/Unleash/unleash">Follow us on Github</a>
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                </tr>
 | 
				
			||||||
 | 
					                                <tr>
 | 
				
			||||||
 | 
					                                    <td valign="top" class="footerContent" style="padding-top:0;" mc:edit="footer_content01">
 | 
				
			||||||
 | 
					                                        <em>Copyright © {{ year }} | Bricks Software | All rights reserved.</em>
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					                                        <strong>Our mailing address is: team@getunleash.io</strong>
 | 
				
			||||||
 | 
					                                        <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
 | 
					                                </tr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            </table>
 | 
				
			||||||
 | 
					                            <!-- // END FOOTER -->
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                </table>
 | 
				
			||||||
 | 
					                <!-- // END TEMPLATE -->
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					</center>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					Scheduled changes can no longer be applied
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{ conflict }}. Scheduled change requests that use this {{ conflictScope }} can no longer be applied and their scheduled applications will fail{{#conflictResolution}}{{.}} ({{conflictResolutionLink}}){{/conflictResolution}}{{^conflictResolution}}.{{/conflictResolution}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For you, this concerns change requests:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{#changeRequests}}
 | 
				
			||||||
 | 
					    - # {{id}} - {{#title}}- {{.}}{{/title}} (scheduled for {{scheduledAt}}) ({{link}})
 | 
				
			||||||
 | 
					{{/changeRequests}}
 | 
				
			||||||
@ -57,7 +57,7 @@ beforeAll(async () => {
 | 
				
			|||||||
        groupService,
 | 
					        groupService,
 | 
				
			||||||
        eventService,
 | 
					        eventService,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const emailService = new EmailService(config.email, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionStore = new SessionStore(
 | 
					    const sessionStore = new SessionStore(
 | 
				
			||||||
        db,
 | 
					        db,
 | 
				
			||||||
        new EventEmitter(),
 | 
					        new EventEmitter(),
 | 
				
			||||||
 | 
				
			|||||||
@ -44,7 +44,7 @@ beforeAll(async () => {
 | 
				
			|||||||
        eventService,
 | 
					        eventService,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const resetTokenService = new ResetTokenService(stores, config);
 | 
					    const resetTokenService = new ResetTokenService(stores, config);
 | 
				
			||||||
    const emailService = new EmailService(undefined, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const sessionService = new SessionService(stores, config);
 | 
					    const sessionService = new SessionService(stores, config);
 | 
				
			||||||
    const settingService = new SettingService(stores, config, eventService);
 | 
					    const settingService = new SettingService(stores, config, eventService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@ beforeAll(async () => {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    resetTokenService = new ResetTokenService(stores, config);
 | 
					    resetTokenService = new ResetTokenService(stores, config);
 | 
				
			||||||
    sessionService = new SessionService(stores, config);
 | 
					    sessionService = new SessionService(stores, config);
 | 
				
			||||||
    const emailService = new EmailService(undefined, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    const settingService = new SettingService(
 | 
					    const settingService = new SettingService(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            settingStore: new FakeSettingStore(),
 | 
					            settingStore: new FakeSettingStore(),
 | 
				
			||||||
 | 
				
			|||||||
@ -43,7 +43,7 @@ beforeAll(async () => {
 | 
				
			|||||||
        eventService,
 | 
					        eventService,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const resetTokenService = new ResetTokenService(stores, config);
 | 
					    const resetTokenService = new ResetTokenService(stores, config);
 | 
				
			||||||
    const emailService = new EmailService(undefined, config.getLogger);
 | 
					    const emailService = new EmailService(config);
 | 
				
			||||||
    sessionService = new SessionService(stores, config);
 | 
					    sessionService = new SessionService(stores, config);
 | 
				
			||||||
    settingService = new SettingService(stores, config, eventService);
 | 
					    settingService = new SettingService(stores, config, eventService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user