diff --git a/Common/Types/API/Hostname.ts b/Common/Types/API/Hostname.ts index 823c955e75..56daac94f2 100644 --- a/Common/Types/API/Hostname.ts +++ b/Common/Types/API/Hostname.ts @@ -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) { diff --git a/Mail/Types/Mail.ts b/Common/Types/Mail/Mail.ts similarity index 50% rename from Mail/Types/Mail.ts rename to Common/Types/Mail/Mail.ts index 89af4196f4..372a3ac8e4 100644 --- a/Mail/Types/Mail.ts +++ b/Common/Types/Mail/Mail.ts @@ -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; diff --git a/Common/Types/Mail/MailServer.ts b/Common/Types/Mail/MailServer.ts new file mode 100644 index 0000000000..38e95ea8a0 --- /dev/null +++ b/Common/Types/Mail/MailServer.ts @@ -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; +} diff --git a/Common/Types/Port.ts b/Common/Types/Port.ts index 78458ead35..d3b865a229 100644 --- a/Common/Types/Port.ts +++ b/Common/Types/Port.ts @@ -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( diff --git a/CommonServer/.env.tpl b/CommonServer/.env.tpl index 82162f670e..11f4805fe8 100644 --- a/CommonServer/.env.tpl +++ b/CommonServer/.env.tpl @@ -1,6 +1,5 @@ ONEUPTIME_SECRET={{ .Env.ONEUPTIME_SECRET }} - DATABASE_PORT={{ .Env.DATABASE_PORT }} DATABASE_USERNAME={{ .Env.DATABASE_USERNAME }} DATABASE_PASSWORD={{ .Env.DATABASE_PASSWORD }} diff --git a/CommonServer/Services/MailService.ts b/CommonServer/Services/MailService.ts index c10e9888c4..41311115c3 100644 --- a/CommonServer/Services/MailService.ts +++ b/CommonServer/Services/MailService.ts @@ -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, - options?: { - projectId?: ObjectID; - forceSendFromGlobalMailServer?: boolean; - } + mail: Mail, + mailServer?: MailServer ): Promise> { 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( new URL( HttpProtocol, MailHostname, - new Route('/email/' + template) + new Route('/email/send') ), body, { diff --git a/Mail/.env.tpl b/Mail/.env.tpl index 1bcbab2a2e..c763c329e0 100644 --- a/Mail/.env.tpl +++ b/Mail/.env.tpl @@ -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 }} diff --git a/Mail/API/Mail.ts b/Mail/API/Mail.ts index 956c0f0c10..18245abe45 100644 --- a/Mail/API/Mail.ts +++ b/Mail/API/Mail.ts @@ -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, - 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; diff --git a/Mail/Services/MailService.ts b/Mail/Services/MailService.ts index ea300f31ab..36371b0348 100755 --- a/Mail/Services/MailService.ts +++ b/Mail/Services/MailService.ts @@ -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 { - 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 { - 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 @@ -169,7 +127,7 @@ export default class MailService { return emailBody(vars).toString(); } - private static compileSubject( + private static compileText( subject: string, vars: Dictionary ): 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 { - 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 { 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 { - 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); } diff --git a/Mail/Types/MailServer.ts b/Mail/Types/MailServer.ts deleted file mode 100644 index f967aedb0c..0000000000 --- a/Mail/Types/MailServer.ts +++ /dev/null @@ -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; -} diff --git a/config.tpl.env b/config.tpl.env index 37a8e70aa9..f3679a5920 100644 --- a/config.tpl.env +++ b/config.tpl.env @@ -80,5 +80,6 @@ SMTP_PORT= SMTP_EMAIL= SMTP_FROM_NAME= SMTP_IS_SECURE= +SMTP_HOST= # Ingress Certificate \ No newline at end of file