fix mailservice

This commit is contained in:
Simon Larsen 2022-11-10 14:56:33 +00:00
parent e7800fe85a
commit 77c22d9e04
No known key found for this signature in database
GPG Key ID: AB45983AA9C81CDE
11 changed files with 161 additions and 300 deletions

View File

@ -18,13 +18,22 @@ export default class Hostname extends DatabaseProperty {
this._port = v;
}
public set hostname(v: string) {
const matchHostnameCharacters: RegExp =
/^[a-zA-Z-\d!#$&'*+,/:;=?@[\].]*$/;
if (v && !matchHostnameCharacters.test(v)) {
throw new BadDataException(`Invalid hostname: ${v}`);
public set hostname(value: string) {
if (Hostname.isValid(value)) {
this._route = value;
} else {
throw new BadDataException('Hostname is not in valid format.');
}
this._route = v;
}
public static isValid(value: string): boolean {
const re: RegExp = /^[a-zA-Z-\d!#$&'*+,/:;=?@[\].]*$/;
const isValid: boolean = re.test(value);
if (!isValid) {
return false;
}
return true;
}
public constructor(hostname: string, port?: Port | string | number) {

View File

@ -1,6 +1,6 @@
import Email from 'Common/Types/Email';
import Dictionary from 'Common/Types/Dictionary';
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
import Email from '../Email';
import Dictionary from '../Dictionary';
import EmailTemplateType from '../Email/EmailTemplateType';
export default interface Mail {
toEmail: Email;

View File

@ -0,0 +1,13 @@
import Email from '../Email';
import Port from '../Port';
import Hostname from '../API/Hostname';
export default interface MailServer {
host: Hostname;
port: Port;
username: string;
password: string;
secure: boolean;
fromEmail: Email;
fromName: string;
}

View File

@ -6,30 +6,42 @@ import Typeof from './Typeof';
export default class Port extends DatabaseProperty {
private _port: PositiveNumber = new PositiveNumber(0);
public get port(): PositiveNumber {
return this._port;
}
public set port(v: PositiveNumber) {
this._port = v;
public set port(value: PositiveNumber) {
if (Port.isValid(value)) {
this._port = value;
} else {
throw new BadDataException('Port is not in valid format.');
}
}
public constructor(port: number | string) {
super();
public static isValid(port: number | string | PositiveNumber): boolean {
if (typeof port === Typeof.String) {
try {
port = Number.parseInt(port.toString(), 10);
} catch (error) {
throw new BadDataException(`Invalid port: ${port}`);
return false;
}
}
if (port >= 0 && port <= 65535) {
this.port = new PositiveNumber(port);
} else {
throw new BadDataException(
'Port should be in the range from 0 to 65535'
);
if (port instanceof PositiveNumber) {
port = port.toNumber();
}
if (port >= 0 && port <= 65535) {
return true
} else {
return false;
}
}
public constructor(port: number | string) {
super();
this.port = new PositiveNumber(port);
}
public static override toDatabase(

View File

@ -1,6 +1,5 @@
ONEUPTIME_SECRET={{ .Env.ONEUPTIME_SECRET }}
DATABASE_PORT={{ .Env.DATABASE_PORT }}
DATABASE_USERNAME={{ .Env.DATABASE_USERNAME }}
DATABASE_PASSWORD={{ .Env.DATABASE_PASSWORD }}

View File

@ -2,45 +2,27 @@ import EmptyResponseData from 'Common/Types/API/EmptyResponse';
import HTTPResponse from 'Common/Types/API/HTTPResponse';
import Route from 'Common/Types/API/Route';
import URL from 'Common/Types/API/URL';
import Dictionary from 'Common/Types/Dictionary';
import Email from 'Common/Types/Email';
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
import { JSONObject } from 'Common/Types/JSON';
import ObjectID from 'Common/Types/ObjectID';
import API from 'Common/Utils/API';
import { ClusterKey, HttpProtocol, MailHostname } from '../Config';
import Mail from 'Common/Types/Mail/Mail';
import MailServer from 'Common/Types/Mail/MailServer';
export default class MailService {
public static async sendMail(
to: Email,
subject: string,
template: EmailTemplateType,
vars: Dictionary<string>,
options?: {
projectId?: ObjectID;
forceSendFromGlobalMailServer?: boolean;
}
mail: Mail,
mailServer?: MailServer
): Promise<HTTPResponse<EmptyResponseData>> {
const body: JSONObject = {
toEmail: to.toString(),
subject,
vars: vars,
...mail,
...mailServer
};
if (options?.projectId) {
body['projectId'] = options.projectId;
}
if (options?.forceSendFromGlobalMailServer) {
body['forceSendFromGlobalMailServer'] =
options.forceSendFromGlobalMailServer;
}
return await API.post<EmptyResponseData>(
new URL(
HttpProtocol,
MailHostname,
new Route('/email/' + template)
new Route('/email/send')
),
body,
{

View File

@ -5,3 +5,4 @@ SMTP_PORT={{ .Env.SMTP_PORT }}
SMTP_EMAIL={{ .Env.SMTP_EMAIL }}
SMTP_FROM_NAME={{ .Env.SMTP_FROM_NAME }}
SMTP_IS_SECURE={{ .Env.SMTP_IS_SECURE }}
SMTP_HOST={{ .Env.SMTP_HOST }}

View File

@ -7,35 +7,43 @@ const router: ExpressRouter = Express.getRouter();
import Response from 'CommonServer/Utils/Response';
import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization';
import MailService from '../Services/MailService';
import Mail from '../Types/Mail';
import Mail from 'Common/Types/Mail/Mail';
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
import { JSONObject } from 'Common/Types/JSON';
import Email from 'Common/Types/Email';
import Dictionary from 'Common/Types/Dictionary';
import ObjectID from 'Common/Types/ObjectID';
import MailServer from 'Common/Types/Mail/MailServer';
router.post(
'/:template-name',
'/send',
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
const mail: Mail = {
templateType: req.params['template-name'] as EmailTemplateType,
toEmail: new Email(body['toEmail'] as string),
templateType: body['template-name'] as EmailTemplateType,
toEmail: new Email(body['to-email'] as string),
subject: body['subject'] as string,
vars: body['vars'] as Dictionary<string>,
body: '',
body: body['body'] as string || '',
};
let mailServer: MailServer | undefined = undefined;
if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getMailServer(req.body);
}
await MailService.send(
mail,
new ObjectID(body['projectId'] as string),
body['forceSendFromGlobalMailServer'] as boolean
mail, mailServer
);
return Response.sendEmptyResponse(req, res);
}
);
const hasMailServerSettingsInBody = (body: JSONObject): boolean => {
return body && Object.keys(body).filter((key) => key.startsWith("SMTP_")).length > 0;
}
export default router;

View File

@ -1,141 +1,99 @@
import nodemailer, { Transporter } from 'nodemailer';
import ObjectID from 'Common/Types/ObjectID';
import hbs from 'nodemailer-express-handlebars';
import Handlebars from 'handlebars';
import fsp from 'fs/promises';
import Mail from '../Types/Mail';
import GlobalConfigService from 'CommonServer/Services/GlobalConfigService';
import ProjectSmtpConfigService from 'CommonServer/Services/ProjectSmtpConfigService';
import EmailLogService from 'CommonServer/Services/EmailLogService';
import Mail from 'Common/Types/Mail/Mail';
import Path from 'path';
import Email from 'Common/Types/Email';
import BadDataException from 'Common/Types/Exception/BadDataException';
import * as Config from '../Config';
import { MailServer } from '../Types/MailServer';
import MailServer from 'Common/Types/Mail/MailServer';
import LocalCache from 'CommonServer/Infrastructure/LocalCache';
import OneUptimeDate from 'Common/Types/Date';
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
import Dictionary from 'Common/Types/Dictionary';
import OperationResult from 'Common/Types/Operation/OperationResult';
import Hostname from 'Common/Types/API/Hostname';
import Exception from 'Common/Types/Exception/Exception';
import GlobalConfig from 'Model/Models/GlobalConfig';
import Port from 'Common/Types/Port';
import ProjectSmtpConfig from 'Model/Models/ProjectSmtpConfig';
import EmailLog from 'Model/Models/EmailLog';
import Project from 'Model/Models/Project';
import { JSONObject } from 'Common/Types/JSON';
import logger from 'CommonServer/Utils/Logger';
export default class MailService {
private static async getGlobalSmtpSettings(): Promise<MailServer> {
const document: GlobalConfig | null =
await GlobalConfigService.findOneBy({
query: {
name: 'smtp',
},
select: {
value: true,
},
props: {
isRoot: true,
},
});
if (document && document.value && !document.value['internalSmtp']) {
return {
username: document.value['email'] as string,
password: document.value['password'] as string,
host: new Hostname(document.value['smtp-server'] as string),
port: new Port(document.value['smtp-port'] as string),
fromEmail: new Email(document.value['from'] as string),
fromName:
(document.value['from-name'] as string) || 'OneUptime',
secure: Boolean(document.value['smtp-secure']),
enabled: Boolean(document.value['email-enabled']),
};
} else if (
document &&
document.value &&
document.value['internalSmtp'] &&
document.value['customSmtp']
) {
return {
username: Config.InternalSmtpUser,
password: Config.InternalSmtpPassword,
host: Config.InternalSmtpHost,
port: Config.InternalSmtpPort,
fromEmail: Config.InternalSmtpFromEmail,
fromName: Config.InternalSmtpFromName,
enabled: Boolean(document.value['email-enabled']),
secure: Config.InternalSmtpSecure,
backupMailServer: {
username: document.value['email'] as string,
password: document.value['password'] as string,
host: new Hostname(document.value['smtp-server'] as string),
port: new Port(document.value['smtp-port'] as string),
fromEmail: new Email(document.value['from'] as string),
fromName:
(document.value['from-name'] as string) || 'OneUptime',
secure: Boolean(document.value['smtp-secure']),
enabled: Boolean(document.value['email-enabled']),
},
};
} else if (
document &&
document.value &&
document.value['internalSmtp']
) {
return {
username: Config.InternalSmtpUser,
password: Config.InternalSmtpPassword,
host: Config.InternalSmtpHost,
port: Config.InternalSmtpPort,
fromEmail: Config.InternalSmtpFromEmail,
fromName: Config.InternalSmtpFromName,
enabled: Boolean(document.value['email-enabled']),
secure: Config.InternalSmtpSecure,
};
public static isSMTPConfigValid(obj: JSONObject): boolean {
if (!obj['SMTP_USERNAME']) {
logger.error('SMTP_USERNAME env var not found');
return false;
}
throw new BadDataException('No Global Settings for Email SMTP found');
}
private static async getProjectSmtpSettings(
projectId: ObjectID
): Promise<MailServer> {
const projectSmtp: ProjectSmtpConfig | null =
await ProjectSmtpConfigService.findOneBy({
query: {
project: new Project(projectId),
},
select: {
username: true,
password: true,
hostname: true,
port: true,
fromName: true,
fromEmail: true,
secure: true,
},
props: {
isRoot: true,
},
});
if (projectSmtp) {
return {
username: projectSmtp.username!,
password: projectSmtp.password!,
host: projectSmtp.hostname!,
port: projectSmtp.port!,
fromName: projectSmtp.fromName! || 'OneUptime',
fromEmail: projectSmtp.fromEmail!,
secure: projectSmtp.secure!,
enabled: true,
};
if (!obj['SMTP_EMAIL']) {
logger.error('SMTP_EMAIL env var not found');
return false;
}
return await this.getGlobalSmtpSettings();
if (!Email.isValid(obj['SMTP_EMAIL'].toString())) {
logger.error('SMTP_EMAIL env var ' + obj['SMTP_EMAIL'] + ' is not a valid email');
return false;
}
if (!obj['SMTP_FROM_NAME']) {
logger.error('SMTP_FROM_NAME env var not found');
return false;
}
if (!obj['SMTP_IS_SECURE']) {
logger.error('SMTP_IS_SECURE env var not found');
return false;
}
if (!obj['SMTP_PORT']) {
logger.error('SMTP_PORT env var not found');
return false;
}
if (!Port.isValid(obj['SMTP_PORT'].toString())) {
logger.error('SMTP_PORT ' + obj['SMTP_HOST'] + ' env var not valid');
return false;
}
if (!obj['SMTP_HOST']) {
logger.error('SMTP_HOST env var not found');
return false;
}
if (!Hostname.isValid(obj['SMTP_HOST'].toString())) {
logger.error('SMTP_HOST env var ' + obj['SMTP_HOST'] + ' not valid');
return false;
}
if (!obj['SMTP_PASSWORD']) {
logger.error('SMTP_PASSWORD env var not found');
return false;
}
return true;
}
public static getMailServer(obj : JSONObject): MailServer {
if (!this.isSMTPConfigValid(obj)) {
throw new BadDataException("SMTP Config is not valid");
}
return {
username: obj['SMTP_USERNAME']?.toString()!,
password: obj['SMTP_PASSWORD']?.toString()!,
host: new Hostname(obj['SMTP_HOST']?.toString()!),
port: new Port(obj['SMTP_PORT']?.toString()!),
fromEmail: new Email(obj['SMTP_EMAIL']?.toString()!),
fromName: obj['SMTP_FROM_NAME']?.toString()!,
secure: obj['SMTP_IS_SECURE'] === "true",
};
}
private static getGlobalSmtpSettings(): MailServer {
return this.getMailServer(process.env);
}
private static async compileEmailBody(
emailTemplateType: EmailTemplateType,
vars: Dictionary<string>
@ -169,7 +127,7 @@ export default class MailService {
return emailBody(vars).toString();
}
private static compileSubject(
private static compileText(
subject: string,
vars: Dictionary<string>
): string {
@ -210,131 +168,24 @@ export default class MailService {
return privateMailer;
}
private static async createEmailStatus(data: {
fromEmail?: Email;
fromName?: string;
toEmail: Email;
subject: string;
body?: string;
templateType?: EmailTemplateType;
status: OperationResult;
smtpHost?: Hostname;
projectId?: ObjectID;
errorDescription?: string;
}): Promise<void> {
const log: EmailLog = new EmailLog();
if (data.fromEmail) {
log.fromEmail = data.fromEmail;
}
if (data.fromName) {
log.fromName = data.fromName;
}
log.toEmail = data.toEmail;
log.subject = data.subject;
if (data.body) {
log.body = data.body;
}
if (data.templateType) {
log.templateType = data.templateType;
}
log.status = data.status;
if (data.smtpHost) {
log.smtpHost = data.smtpHost;
}
if (data.errorDescription) {
log.errorDescription = data.errorDescription;
}
if (data.projectId) {
log.project = new Project(data.projectId);
}
await EmailLogService.create({
data: log,
props: {
isRoot: true,
},
});
}
private static async transportMail(
mail: Mail,
mailServer: MailServer
): Promise<void> {
const mailer: Transporter = this.createMailer(mailServer);
try {
await mailer.sendMail(mail);
await this.createEmailStatus({
fromEmail: mailServer.fromEmail,
fromName: mailServer.fromName,
smtpHost: mailServer.host,
toEmail: mail.toEmail,
subject: mail.subject,
templateType: mail.templateType,
body: mail.body,
status: OperationResult.Success,
});
} catch (error) {
if (mailServer.backupMailServer) {
return await this.transportMail(
mail,
mailServer.backupMailServer
);
}
const exception: Exception = error as Exception;
await this.createEmailStatus({
fromEmail: mailServer.fromEmail,
fromName: mailServer.fromName,
smtpHost: mailServer.host,
toEmail: mail.toEmail,
subject: mail.subject,
templateType: mail.templateType,
body: mail.body,
status: OperationResult.Error,
errorDescription: exception.message,
});
}
await mailer.sendMail(mail);
}
public static async send(
mail: Mail,
projectId?: ObjectID,
forceSendFromGlobalMailServer?: boolean
mailServer?: MailServer
): Promise<void> {
let mailServer: MailServer | null = null;
if (forceSendFromGlobalMailServer) {
mailServer = await this.getGlobalSmtpSettings();
}
if (projectId && !forceSendFromGlobalMailServer) {
mailServer = await this.getProjectSmtpSettings(projectId);
}
if (!mailServer) {
await this.createEmailStatus({
toEmail: mail.toEmail,
subject: mail.subject,
templateType: mail.templateType,
status: OperationResult.Error,
errorDescription: 'SMTP settings not found',
});
throw new BadDataException('SMTP settings not found');
mailServer = this.getGlobalSmtpSettings();
}
mail.body = await this.compileEmailBody(mail.templateType, mail.vars);
mail.subject = this.compileSubject(mail.subject, mail.vars);
mail.body = mail.templateType ? await this.compileEmailBody(mail.templateType, mail.vars) : this.compileText(mail.body, mail.vars);
mail.subject = this.compileText(mail.subject, mail.vars);
await this.transportMail(mail, mailServer);
}

View File

@ -1,15 +0,0 @@
import Email from 'Common/Types/Email';
import Port from 'Common/Types/Port';
import Hostname from 'Common/Types/API/Hostname';
export interface MailServer {
host: Hostname;
port: Port;
username: string;
password: string;
secure: boolean;
fromEmail: Email;
fromName: string;
enabled: boolean;
backupMailServer?: MailServer;
}

View File

@ -80,5 +80,6 @@ SMTP_PORT=
SMTP_EMAIL=
SMTP_FROM_NAME=
SMTP_IS_SECURE=
SMTP_HOST=
# Ingress Certificate