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 { EmailService } from './email-service'; | ||||
| import noLoggerProvider from '../../test/fixtures/no-logger'; | ||||
| import { IUnleashConfig } from '../types'; | ||||
| 
 | ||||
| test('Can send reset email', async () => { | ||||
|     const emailService = new EmailService( | ||||
|         { | ||||
|     const emailService = new EmailService({ | ||||
|         email: { | ||||
|             host: 'test', | ||||
|             port: 587, | ||||
|             secure: false, | ||||
| @ -12,8 +13,8 @@ test('Can send reset email', async () => { | ||||
|             smtppass: '', | ||||
|             sender: 'noreply@getunleash.ai', | ||||
|         }, | ||||
|         noLoggerProvider, | ||||
|     ); | ||||
|         getLogger: noLoggerProvider, | ||||
|     } as unknown as IUnleashConfig); | ||||
|     const resetLinkUrl = | ||||
|         '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 () => { | ||||
|     const emailService = new EmailService( | ||||
|         { | ||||
|     const emailService = new EmailService({ | ||||
|         email: { | ||||
|             host: 'test', | ||||
|             port: 9999, | ||||
|             port: 587, | ||||
|             secure: false, | ||||
|             sender: 'noreply@getunleash.ai', | ||||
|             smtpuser: '', | ||||
|             smtppass: '', | ||||
|             sender: 'noreply@getunleash.ai', | ||||
|         }, | ||||
|         noLoggerProvider, | ||||
|     ); | ||||
|         getLogger: noLoggerProvider, | ||||
|     } as unknown as IUnleashConfig); | ||||
|     const content = await emailService.sendGettingStartedMail( | ||||
|         'Some username', | ||||
|         'test@test.com', | ||||
| @ -52,8 +53,8 @@ test('Can send welcome mail', async () => { | ||||
| test('Can supply additional SMTP transport options', async () => { | ||||
|     const spy = jest.spyOn(nodemailer, 'createTransport'); | ||||
| 
 | ||||
|     new EmailService( | ||||
|         { | ||||
|     new EmailService({ | ||||
|         email: { | ||||
|             host: 'smtp.unleash.test', | ||||
|             port: 9999, | ||||
|             secure: false, | ||||
| @ -64,8 +65,8 @@ test('Can supply additional SMTP transport options', async () => { | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         noLoggerProvider, | ||||
|     ); | ||||
|         getLogger: noLoggerProvider, | ||||
|     } as unknown as IUnleashConfig); | ||||
| 
 | ||||
|     expect(spy).toHaveBeenCalledWith({ | ||||
|         auth: { | ||||
| @ -82,17 +83,17 @@ test('Can supply additional SMTP transport options', async () => { | ||||
| }); | ||||
| 
 | ||||
| test('should strip special characters from email subject', async () => { | ||||
|     const emailService = new EmailService( | ||||
|         { | ||||
|     const emailService = new EmailService({ | ||||
|         email: { | ||||
|             host: 'test', | ||||
|             port: 9999, | ||||
|             port: 587, | ||||
|             secure: false, | ||||
|             sender: 'noreply@getunleash.ai', | ||||
|             smtpuser: '', | ||||
|             smtppass: '', | ||||
|             sender: 'noreply@getunleash.ai', | ||||
|         }, | ||||
|         noLoggerProvider, | ||||
|     ); | ||||
|         getLogger: noLoggerProvider, | ||||
|     } as unknown as IUnleashConfig); | ||||
|     expect(emailService.stripSpecialCharacters('http://evil.com')).toBe( | ||||
|         'httpevilcom', | ||||
|     ); | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { createTransport, Transporter } from 'nodemailer'; | ||||
| import Mustache from 'mustache'; | ||||
| import path from 'path'; | ||||
| import { readFileSync, existsSync } from 'fs'; | ||||
| import { Logger, LogProvider } from '../logger'; | ||||
| import { existsSync, readFileSync } from 'fs'; | ||||
| import { Logger } from '../logger'; | ||||
| import NotFoundError from '../error/notfound-error'; | ||||
| import { IEmailOption } from '../types/option'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| 
 | ||||
| export interface IAuthOptions { | ||||
|     user: string; | ||||
| @ -31,6 +31,8 @@ export interface IEmailEnvelope { | ||||
| 
 | ||||
| const RESET_MAIL_SUBJECT = 'Unleash - Reset your password'; | ||||
| 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 = | ||||
|     'Unleash - Scheduled change request could not be applied'; | ||||
| 
 | ||||
| @ -38,13 +40,16 @@ export const MAIL_ACCEPTED = '250 Accepted'; | ||||
| 
 | ||||
| export class EmailService { | ||||
|     private logger: Logger; | ||||
|     private config: IUnleashConfig; | ||||
| 
 | ||||
|     private readonly mailer?: Transporter; | ||||
| 
 | ||||
|     private readonly sender: string; | ||||
| 
 | ||||
|     constructor(email: IEmailOption | undefined, getLogger: LogProvider) { | ||||
|         this.logger = getLogger('services/email-service.ts'); | ||||
|     constructor(config: IUnleashConfig) { | ||||
|         this.config = config; | ||||
|         this.logger = config.getLogger('services/email-service.ts'); | ||||
|         const { email } = config; | ||||
|         if (email?.host) { | ||||
|             this.sender = email.sender; | ||||
|             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( | ||||
|         name: string, | ||||
|         recipient: string, | ||||
|  | ||||
| @ -140,7 +140,7 @@ export const createServices = ( | ||||
|         eventService, | ||||
|         privateProjectChecker, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const featureTypeService = new FeatureTypeService( | ||||
|         stores, | ||||
|         config, | ||||
|  | ||||
| @ -32,7 +32,7 @@ test('Should create new user', async () => { | ||||
|     ); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const eventService = new EventService( | ||||
|         { eventStore, featureTagStore: new FakeFeatureTagStore() }, | ||||
|         config, | ||||
| @ -78,7 +78,7 @@ test('Should create default user - with defaults', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -117,7 +117,7 @@ test('Should create default user - with provided username and password', async ( | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -161,7 +161,7 @@ test('Should not create default user - with `createAdminUser` === false', async | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -210,7 +210,7 @@ test('Should be a valid password', async () => { | ||||
|         config, | ||||
|     ); | ||||
| 
 | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -248,7 +248,7 @@ test('Password must be at least 10 chars', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -288,7 +288,7 @@ test('The password must contain at least one uppercase letter.', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -330,7 +330,7 @@ test('The password must contain at least one number', async () => { | ||||
|         config, | ||||
|     ); | ||||
| 
 | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -371,7 +371,7 @@ test('The password must contain at least one special character', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -412,7 +412,7 @@ test('Should be a valid password with special chars', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -450,7 +450,7 @@ test('Should send password reset email if user exists', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const eventService = new EventService( | ||||
| @ -504,7 +504,7 @@ test('Should throttle password reset email', async () => { | ||||
|         { resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     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, | ||||
|         eventService, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const sessionStore = new SessionStore( | ||||
|         db, | ||||
|         new EventEmitter(), | ||||
|  | ||||
| @ -44,7 +44,7 @@ beforeAll(async () => { | ||||
|         eventService, | ||||
|     ); | ||||
|     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 settingService = new SettingService(stores, config, eventService); | ||||
| 
 | ||||
|  | ||||
| @ -38,7 +38,7 @@ beforeAll(async () => { | ||||
|     ); | ||||
|     resetTokenService = new ResetTokenService(stores, config); | ||||
|     sessionService = new SessionService(stores, config); | ||||
|     const emailService = new EmailService(undefined, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     const settingService = new SettingService( | ||||
|         { | ||||
|             settingStore: new FakeSettingStore(), | ||||
|  | ||||
| @ -43,7 +43,7 @@ beforeAll(async () => { | ||||
|         eventService, | ||||
|     ); | ||||
|     const resetTokenService = new ResetTokenService(stores, config); | ||||
|     const emailService = new EmailService(undefined, config.getLogger); | ||||
|     const emailService = new EmailService(config); | ||||
|     sessionService = new SessionService(stores, config); | ||||
|     settingService = new SettingService(stores, config, eventService); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user