unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
354 lines • 16.1 kB
JavaScript
;
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