UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

354 lines • 16.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmailService = exports.MAIL_ACCEPTED = exports.TransporterType = exports.TemplateFormat = void 0; const nodemailer_1 = require("nodemailer"); const mustache_1 = __importDefault(require("mustache")); const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const notfound_error_1 = __importDefault(require("../error/notfound-error")); const productivity_report_view_model_1 = require("../features/productivity-report/productivity-report-view-model"); var TemplateFormat; (function (TemplateFormat) { TemplateFormat["HTML"] = "html"; TemplateFormat["PLAIN"] = "plain"; })(TemplateFormat || (exports.TemplateFormat = TemplateFormat = {})); var TransporterType; (function (TransporterType) { TransporterType["SMTP"] = "smtp"; TransporterType["JSON"] = "json"; })(TransporterType || (exports.TransporterType = TransporterType = {})); const RESET_MAIL_SUBJECT = 'Unleash - Reset your password'; const GETTING_STARTED_SUBJECT = 'Welcome to Unleash'; const ORDER_ENVIRONMENTS_SUBJECT = 'Unleash - ordered environments successfully'; const PRODUCTIVITY_REPORT = 'Unleash - productivity report'; 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'; exports.MAIL_ACCEPTED = '250 Accepted'; class EmailService { constructor(config) { 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') { this.mailer = (0, nodemailer_1.createTransport)({ jsonTransport: true }); } else { this.mailer = (0, nodemailer_1.createTransport)({ host: email.host, port: email.port, secure: email.secure, auth: { user: email.smtpuser ?? '', pass: email.smtppass ?? '', }, ...email.transportOptions, }); } this.logger.info(`Initialized transport to ${email.host} on port ${email.port} with user: ${email.smtpuser}`); } else { this.sender = 'not-configured'; this.mailer = undefined; } } async sendScheduledExecutionFailedEmail(recipient, changeRequestLink, changeRequestTitle, scheduledAt, errorMessage) { if (this.configured()) { const year = new Date().getFullYear(); const bodyHtml = await this.compileTemplate('scheduled-execution-failed', TemplateFormat.HTML, { changeRequestLink, changeRequestTitle, scheduledAt, errorMessage, year, }); const bodyText = await this.compileTemplate('scheduled-execution-failed', TemplateFormat.PLAIN, { changeRequestLink, changeRequestTitle, scheduledAt, errorMessage, year, }); const email = { from: this.sender, to: recipient, subject: SCHEDULED_EXECUTION_FAILED_SUBJECT, html: bodyHtml, text: bodyText, }; process.nextTick(() => { this.mailer.sendMail(email).then(() => this.logger.info('Successfully sent scheduled-execution-failed email'), (e) => this.logger.warn('Failed to send scheduled-execution-failed 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'); this.logger.debug('Change request link: ', changeRequestLink); res({ from: this.sender, to: recipient, subject: SCHEDULED_EXECUTION_FAILED_SUBJECT, html: '', text: '', }); }); } async sendScheduledChangeConflictEmail(recipient, conflictScope, conflictingChangeRequestId, changeRequests, flagName, project, strategyId) { const conflictData = conflictScope === 'flag' ? { reason: 'flag archived', flagName } : { reason: 'strategy deleted', flagName, strategyId: strategyId ?? '', }; return this.sendScheduledChangeSuspendedEmail(recipient, conflictData, conflictingChangeRequestId, changeRequests, project); } async sendScheduledChangeSuspendedEmail(recipient, conflictData, conflictingChangeRequestId, changeRequests, project) { if (this.configured()) { const year = new Date().getFullYear(); const getConflictDetails = () => { switch (conflictData.reason) { case 'flag archived': return { conflictScope: 'flag', conflict: `The feature flag ${conflictData.flagName} in ${project} has been archived`, flagArchived: true, flagLink: `${this.config.server.unleashUrl}/projects/${project}/archive?sort=archivedAt&search=${conflictData.flagName}`, canBeRescheduled: false, }; case 'strategy deleted': return { conflictScope: 'strategy', conflict: `The strategy with id ${conflictData.strategyId} for flag ${conflictData.flagName} in ${project} has been deleted`, canBeRescheduled: false, }; case 'strategy updated': return { conflictScope: 'strategy', conflict: `A strategy belonging to ${conflictData.flagName} (ID: ${conflictData.strategyId}) in the project ${project} has been updated, and your changes would overwrite some of the recent changes`, canBeRescheduled: true, }; case 'environment variants updated': return { conflictScope: 'environment variant configuration', conflict: `The ${conflictData.environment} environment variant configuration for ${conflictData.flagName} in the project ${project} has been updated, and your changes would overwrite some of the recent changes`, canBeRescheduled: true, }; case 'segment updated': return { conflictScope: 'segment', conflict: `Segment ${conflictData.segment.id} ("${conflictData.segment.name}") in ${project} has been updated, and your changes would overwrite some of the recent changes`, canBeRescheduled: true, }; } }; const { canBeRescheduled, conflict, conflictScope, flagArchived = false, flagLink = false, } = getConflictDetails(); const conflictingChangeRequestLink = conflictingChangeRequestId ? `${this.config.server.unleashUrl}/projects/${project}/change-requests/${conflictingChangeRequestId}` : false; const bodyHtml = await this.compileTemplate('scheduled-change-conflict', TemplateFormat.HTML, { conflict, conflictScope, canBeRescheduled, flagArchived, flagLink, conflictingChangeRequestLink, changeRequests, year, }); const bodyText = await this.compileTemplate('scheduled-change-conflict', TemplateFormat.PLAIN, { conflict, conflictScope, canBeRescheduled, flagArchived, flagLink, conflictingChangeRequestLink, 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, recipient, resetLink) { if (this.configured()) { const year = new Date().getFullYear(); const bodyHtml = await this.compileTemplate('reset-password', TemplateFormat.HTML, { resetLink, name, year, }); const bodyText = await this.compileTemplate('reset-password', TemplateFormat.PLAIN, { resetLink, name, year, }); const email = { from: this.sender, to: recipient, subject: RESET_MAIL_SUBJECT, html: bodyHtml, text: bodyText, }; process.nextTick(() => { this.mailer.sendMail(email).then(() => this.logger.info('Successfully sent reset-password email'), (e) => this.logger.warn('Failed to send reset-password 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 emailservice'); this.logger.debug('Reset link: ', resetLink); res({ from: this.sender, to: recipient, subject: RESET_MAIL_SUBJECT, html: '', text: '', }); }); } async sendGettingStartedMail(name, recipient, unleashUrl, passwordLink) { if (this.configured()) { const year = new Date().getFullYear(); const context = { passwordLink, name: this.stripSpecialCharacters(name), year, unleashUrl, }; const bodyHtml = await this.compileTemplate('getting-started', TemplateFormat.HTML, context); const bodyText = await this.compileTemplate('getting-started', TemplateFormat.PLAIN, context); const email = { from: this.sender, to: recipient, subject: GETTING_STARTED_SUBJECT, html: bodyHtml, text: bodyText, }; process.nextTick(() => { this.mailer.sendMail(email).then(() => this.logger.info('Successfully sent getting started email'), (e) => this.logger.warn('Failed to send getting started 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 EmailService'); res({ from: this.sender, to: recipient, subject: GETTING_STARTED_SUBJECT, html: '', text: '', }); }); } async sendProductivityReportEmail(userEmail, userName, metrics) { if (this.configured()) { const context = (0, productivity_report_view_model_1.productivityReportViewModel)({ metrics, userEmail, userName, unleashUrl: this.config.server.unleashUrl, }); const template = 'productivity-report'; const bodyHtml = await this.compileTemplate(template, TemplateFormat.HTML, context); const bodyText = await this.compileTemplate(template, TemplateFormat.PLAIN, context); const headers = {}; Object.entries(this.config.email.optionalHeaders || {}).forEach(([key, value]) => { if (typeof value === 'string') { headers[key] = value; } }); const email = { from: this.sender, to: userEmail, bcc: '', subject: PRODUCTIVITY_REPORT, html: bodyHtml, text: bodyText, attachments: [ this.resolveTemplateAttachment(template, 'unleash-logo.png', 'unleashLogo'), ], headers, }; process.nextTick(() => { this.mailer.sendMail(email).then(() => this.logger.info('Successfully sent productivity report email'), (e) => this.logger.warn('Failed to send productivity report 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: userEmail, bcc: '', subject: PRODUCTIVITY_REPORT, html: '', text: '', }); }); } isEnabled() { return this.mailer !== undefined; } async compileTemplate(templateName, format, context) { try { const template = this.resolveTemplate(templateName, format); return await Promise.resolve(mustache_1.default.render(template, context)); } catch (e) { this.logger.info(`Could not find template ${templateName}`); return Promise.reject(e); } } resolveTemplate(templateName, format) { const topPath = path_1.default.resolve(__dirname, '../../mailtemplates'); const template = path_1.default.join(topPath, templateName, `${templateName}.${format}.mustache`); if ((0, fs_1.existsSync)(template)) { return (0, fs_1.readFileSync)(template, 'utf-8'); } throw new notfound_error_1.default('Could not find template'); } resolveTemplateAttachment(templateName, filename, cid) { const topPath = path_1.default.resolve(__dirname, '../../mailtemplates'); const attachment = path_1.default.join(topPath, templateName, filename); if ((0, fs_1.existsSync)(attachment)) { return { filename, path: attachment, cid, }; } throw new notfound_error_1.default('Could not find email attachment'); } configured() { return this.sender !== 'not-configured' && this.mailer !== undefined; } stripSpecialCharacters(str) { return str?.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[\]\\/]/gi, ''); } } exports.EmailService = EmailService; //# sourceMappingURL=email-service.js.map