feat: allow env variables to overwrite smtp config

Fixes #3
This commit is contained in:
Anselm Stordeur
2019-01-09 17:53:53 +01:00
parent cdbbb0ae1f
commit 3d82c94577
4 changed files with 199 additions and 84 deletions

View File

@@ -8,28 +8,30 @@ This is a simple logger for TypeScript projects with colors for console output.
Logs are only printed if their log level is equal or higher than the defined log level.
## Log Levels
Available Log levels are:
Available log levels are:
- 1 - INFO
- 2 - LOG
- 4 - WARN
- 8 - ERROR
- 16 - OK
You can set your Log Level with the environment variable
`STAPPS_LOG_LEVEL` in a binary way.
You can set your log level with the environment variable `STAPPS_LOG_LEVEL`.
For example `STAPPS_LOG_LEVEL=17` is 16 + 1 and would log everything
that is `OK` or `ERROR`.
To select your desired log levels add the corresponding numbers and set the value of `STAPPS_LOG_LEVEL` to the sum.
If you want to use Logger in production (`NODE_ENV=production`) and allow all transports to fail set `ALLOW_NO_TRANSPORT` to `true`.
For example `STAPPS_LOG_LEVEL=17` is 16 + 1 and would log everything that is `OK` or `INFO`.
If you want to use logger in production (`NODE_ENV=production`) and allow all transports to fail set
`ALLOW_NO_TRANSPORT` to `true`.
## SMTP
This class also provides a simple implementation of an smtp transport which can be used as a
This class also provides a simple implementation of an SMTP transport which can be used as a
`TransportWithVerification` for the logger. You can use this to transport errors of the logger or to transport mails
of your own monitoring solution.
### Usage
You can instatiate it with a config or it will check for a config in the environment variables.
You can instatiate it with a config or it will check for a config in the environment variables. Environment variables
can overwrite the actual config values.
Environment variables are:
* SMTP_AUTH_USER: SMTP username

View File

@@ -14,6 +14,7 @@
*/
import * as nodemailer from 'nodemailer';
import { MailOptions } from 'nodemailer/lib/sendmail-transport';
import { deleteUndefinedProperties, RecursivePartial } from './common';
import { Logger } from './Logger';
import { TransportWithVerification } from './Transport';
@@ -93,55 +94,6 @@ export class SMTP extends TransportWithVerification {
return this._instance;
}
// if no config is given, we assume it is set via environment variables
if (typeof config === 'undefined') {
if (typeof process.env.SMTP_AUTH_USER !== 'string') {
throw new Error('User of SMTP configuration is empty. Please provide a user.');
}
if (typeof process.env.SMTP_AUTH_PASSWORD !== 'string') {
throw new Error('Password of SMTP configuration is empty. Please provide a password.');
}
if (typeof process.env.SMTP_HOST !== 'string') {
throw new Error('Host of SMTP configuration is empty. Please provide a host.');
}
if (typeof process.env.SMTP_PORT !== 'string') {
throw new Error('Port of SMTP configuration is empty. Please provide a port.');
}
if (typeof process.env.SMTP_RECIPIENTS !== 'string') {
throw new Error('Recipients of SMTP configuration is empty. Please provide at least one recipient.');
}
if (typeof process.env.SMTP_SENDER_MAIL !== 'string') {
throw new Error('Sender of SMTP configuration is empty. Please provide a sender.');
}
if (typeof process.env.SMTP_SENDER_NAME !== 'string') {
throw new Error('Name of sender of SMTP configuration is empty. Please provide a name for the sender.');
}
config = {
auth: {
password: process.env.SMTP_AUTH_PASSWORD,
user: process.env.SMTP_AUTH_USER,
},
cc: ((typeof process.env.SMTP_CC === 'string') ? (process.env.SMTP_CC as string).split(',') : []),
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT, 10),
recipients: (typeof process.env.SMTP_RECIPIENTS === 'string') ?
(process.env.SMTP_RECIPIENTS).split(',') :
[],
secure: (typeof process.env.SMTP_SECURE === 'string') ? (process.env.SMTP_SECURE === 'true') : false,
sender: {
mail: process.env.SMTP_SENDER_MAIL,
name: process.env.SMTP_SENDER_NAME,
},
};
}
// monitoring is not required -> SMTP init can fail
if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') {
try {
@@ -185,7 +137,7 @@ export class SMTP extends TransportWithVerification {
* @param {string[]} recipients
* @return {string[]}
*/
public static isValidRecipientsList(recipients: string[]): boolean {
public static isValidRecipientsList(recipients: string[] | undefined): boolean {
return Array.isArray(recipients) && recipients.length > 0 && recipients.every(this.isValidEmailAddress);
}
@@ -194,46 +146,88 @@ export class SMTP extends TransportWithVerification {
* call `new SMTP()`
* @param {SMTPConfig} config
*/
private constructor(config: SMTPConfig) {
if (typeof config.host !== 'string') {
throw new Error('SMTP configuration needs a host.');
private constructor(config?: SMTPConfig) {
// create a partial config from environment variables that can overwrite the given config
const envConfig: RecursivePartial<SMTPConfig> = {
auth: {
password: process.env.SMTP_AUTH_PASSWORD,
user: process.env.SMTP_AUTH_USER,
},
cc: ((typeof process.env.SMTP_CC === 'string') ? (process.env.SMTP_CC as string).split(',') : []),
host: process.env.SMTP_HOST,
port: (typeof process.env.SMTP_PORT === 'string') ? parseInt(process.env.SMTP_PORT, 10) : undefined,
recipients: (typeof process.env.SMTP_RECIPIENTS === 'string') ?
(process.env.SMTP_RECIPIENTS).split(',') :
[],
secure: (typeof process.env.SMTP_SECURE === 'string') ? (process.env.SMTP_SECURE === 'true') : false,
sender: {
mail: process.env.SMTP_SENDER_MAIL,
name: process.env.SMTP_SENDER_NAME,
},
};
// @ts-ignore
config = {
...config,
// deleting undefined properties so the actual config doesn't get overwritten by undefined values
...deleteUndefinedProperties(envConfig),
};
if (typeof config!.host !== 'string') {
throw new Error(
'SMTP configuration needs a host. Add it to the config or use environment variables (SMTP_HOST).',
);
}
if (typeof config.port !== 'number') {
throw new Error('SMTP configuration needs a port.');
if (typeof config!.port !== 'number' || isNaN(config!.port)) {
throw new Error(
'SMTP configuration needs a port. Add it to the config or use environment variables (SMTP_PORT).',
);
}
if (typeof config.auth !== 'object') {
throw new Error('SMTP configuration needs an auth object.');
if (typeof config!.auth !== 'object') {
throw new Error(
'SMTP configuration needs an auth object.' +
'Add it to the config or use environment variables (SMTP_AUTH_USER, SMTP_AUTH_PASSWORD).',
);
}
if (typeof config.auth.user !== 'string') {
throw new Error('SMTP auth configuration needs a user');
if (typeof config!.auth.user !== 'string') {
throw new Error(
'SMTP auth configuration needs a user. Add it to the config or use environment variables (SMTP_AUTH_USER).',
);
}
if (typeof config.auth.password !== 'string') {
throw new Error('SMTP auth configuration needs a password');
if (typeof config!.auth.password !== 'string') {
throw new Error(
'SMTP auth configuration needs a password.' +
'Add it to the config or use environment variables (SMTP_AUTH_PASSWORD).',
);
}
if (Array.isArray(config.recipients) && config.recipients.length < 1) {
throw new Error('SMTP configuration needs recipients.');
if (Array.isArray(config!.recipients) && config!.recipients.length < 1) {
throw new Error(
'SMTP configuration needs recipients. Add it to the config or use environment variables (SMTP_RECIPIENTS).',
);
}
if (typeof config.sender.mail !== 'string') {
throw new Error('SMTP configuration needs a sender');
if (typeof config!.sender.mail !== 'string') {
throw new Error(
'SMTP configuration needs a sender. Add it to the config or use environment variables (SMTP_SENDER_MAIL).',
);
}
super();
if (SMTP.isValidRecipientsList(config.recipients)) {
this.recipients = config.recipients;
if (SMTP.isValidRecipientsList(config!.recipients)) {
this.recipients = config!.recipients;
} else {
throw new Error('Invalid recipients found');
}
if (typeof config.cc !== 'undefined') {
if (SMTP.isValidRecipientsList(config.cc) || (Array.isArray(config.cc) && config.cc.length === 0)) {
this.cc = config.cc;
if (typeof config!.cc !== 'undefined') {
if (SMTP.isValidRecipientsList(config!.cc)) {
this.cc = config!.cc!;
} else {
throw new Error('Invalid cc recipients found');
}
@@ -241,19 +235,19 @@ export class SMTP extends TransportWithVerification {
this.cc = [];
}
this.from = config.sender;
this.from = config!.sender;
this.verified = false;
// creating transport with configuration
this.transportAgent = nodemailer.createTransport({
auth: {
pass: config.auth.password,
user: config.auth.user,
pass: config!.auth.password,
user: config!.auth.user,
},
host: config.host,
port: config.port,
secure: typeof config.secure === 'boolean' ? config.secure : false,
host: config!.host,
port: config!.port,
secure: typeof config!.secure === 'boolean' ? config!.secure : false,
});
}

49
src/common.ts Normal file
View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* A recursive partial object
*
* Copied from https://stackoverflow.com/a/51365037
*/
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U> ?
Array<RecursivePartial<U>> :
T[P] extends object ? RecursivePartial<T[P]> : T[P];
};
/**
* Deletes all properties that are undefined from an object
* @param obj
*/
export function deleteUndefinedProperties(obj: any) {
// return atomic data types and arrays (recursion anchor)
if (typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
// check each key
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'undefined') {
// delete undefined keys
delete obj[key];
} else {
// check recursive
obj[key] = deleteUndefinedProperties(obj[key]);
}
});
return obj;
}

70
test/Common.spec.ts Normal file
View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { expect } from 'chai';
import { slow, suite, test, timeout } from 'mocha-typescript';
import { deleteUndefinedProperties } from '../src/common';
@suite(timeout(2000), slow(1000))
export class CommonSpec {
/* tslint:disable:member-ordering */
@test
deleteUndefinedProperties1() {
expect(deleteUndefinedProperties(
{
a: 2,
b: {
c: 3,
d: undefined,
},
},
)).to.deep.equal(
{
a: 2,
b: {
c: 3,
},
},
);
}
@test
deleteUndefinedProperties2() {
expect(deleteUndefinedProperties(
{
a: undefined,
b: undefined,
},
)).to.deep.equal(
{},
);
}
@test
deleteUndefinedProperties3() {
expect(deleteUndefinedProperties(
{
a: 2,
b: 'foo',
c: 'bar',
},
)).to.deep.equal(
{
a: 2,
b: 'foo',
c: 'bar',
},
);
}
}