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. Logs are only printed if their log level is equal or higher than the defined log level.
## Log Levels ## Log Levels
Available Log levels are: Available log levels are:
- 1 - INFO - 1 - INFO
- 2 - LOG - 2 - LOG
- 4 - WARN - 4 - WARN
- 8 - ERROR - 8 - ERROR
- 16 - OK - 16 - OK
You can set your Log Level with the environment variable You can set your log level with the environment variable `STAPPS_LOG_LEVEL`.
`STAPPS_LOG_LEVEL` in a binary way.
For example `STAPPS_LOG_LEVEL=17` is 16 + 1 and would log everything To select your desired log levels add the corresponding numbers and set the value of `STAPPS_LOG_LEVEL` to the sum.
that is `OK` or `ERROR`.
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 ## 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 `TransportWithVerification` for the logger. You can use this to transport errors of the logger or to transport mails
of your own monitoring solution. of your own monitoring solution.
### Usage ### 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: Environment variables are:
* SMTP_AUTH_USER: SMTP username * SMTP_AUTH_USER: SMTP username

View File

@@ -14,6 +14,7 @@
*/ */
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import { MailOptions } from 'nodemailer/lib/sendmail-transport'; import { MailOptions } from 'nodemailer/lib/sendmail-transport';
import { deleteUndefinedProperties, RecursivePartial } from './common';
import { Logger } from './Logger'; import { Logger } from './Logger';
import { TransportWithVerification } from './Transport'; import { TransportWithVerification } from './Transport';
@@ -93,55 +94,6 @@ export class SMTP extends TransportWithVerification {
return this._instance; 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 // monitoring is not required -> SMTP init can fail
if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') { if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') {
try { try {
@@ -185,7 +137,7 @@ export class SMTP extends TransportWithVerification {
* @param {string[]} recipients * @param {string[]} recipients
* @return {string[]} * @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); return Array.isArray(recipients) && recipients.length > 0 && recipients.every(this.isValidEmailAddress);
} }
@@ -194,46 +146,88 @@ export class SMTP extends TransportWithVerification {
* call `new SMTP()` * call `new SMTP()`
* @param {SMTPConfig} config * @param {SMTPConfig} config
*/ */
private constructor(config: SMTPConfig) { private constructor(config?: SMTPConfig) {
if (typeof config.host !== 'string') { // create a partial config from environment variables that can overwrite the given config
throw new Error('SMTP configuration needs a host.'); 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') { if (typeof config!.port !== 'number' || isNaN(config!.port)) {
throw new Error('SMTP configuration needs a 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') { if (typeof config!.auth !== 'object') {
throw new Error('SMTP configuration needs an 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') { if (typeof config!.auth.user !== 'string') {
throw new Error('SMTP auth configuration needs a user'); 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') { if (typeof config!.auth.password !== 'string') {
throw new Error('SMTP auth configuration needs a password'); 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) { if (Array.isArray(config!.recipients) && config!.recipients.length < 1) {
throw new Error('SMTP configuration needs recipients.'); throw new Error(
'SMTP configuration needs recipients. Add it to the config or use environment variables (SMTP_RECIPIENTS).',
);
} }
if (typeof config.sender.mail !== 'string') { if (typeof config!.sender.mail !== 'string') {
throw new Error('SMTP configuration needs a sender'); throw new Error(
'SMTP configuration needs a sender. Add it to the config or use environment variables (SMTP_SENDER_MAIL).',
);
} }
super(); super();
if (SMTP.isValidRecipientsList(config.recipients)) { if (SMTP.isValidRecipientsList(config!.recipients)) {
this.recipients = config.recipients; this.recipients = config!.recipients;
} else { } else {
throw new Error('Invalid recipients found'); throw new Error('Invalid recipients found');
} }
if (typeof config.cc !== 'undefined') { if (typeof config!.cc !== 'undefined') {
if (SMTP.isValidRecipientsList(config.cc) || (Array.isArray(config.cc) && config.cc.length === 0)) { if (SMTP.isValidRecipientsList(config!.cc)) {
this.cc = config.cc; this.cc = config!.cc!;
} else { } else {
throw new Error('Invalid cc recipients found'); throw new Error('Invalid cc recipients found');
} }
@@ -241,19 +235,19 @@ export class SMTP extends TransportWithVerification {
this.cc = []; this.cc = [];
} }
this.from = config.sender; this.from = config!.sender;
this.verified = false; this.verified = false;
// creating transport with configuration // creating transport with configuration
this.transportAgent = nodemailer.createTransport({ this.transportAgent = nodemailer.createTransport({
auth: { auth: {
pass: config.auth.password, pass: config!.auth.password,
user: config.auth.user, user: config!.auth.user,
}, },
host: config.host, host: config!.host,
port: config.port, port: config!.port,
secure: typeof config.secure === 'boolean' ? config.secure : false, 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',
},
);
}
}