Skip to content

Commit

Permalink
Prepare the settings for email sending
Browse files Browse the repository at this point in the history
  • Loading branch information
floscher committed Jun 25, 2024
1 parent 9433cf1 commit 682104b
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ A blog software written in Typescript, with an Express.js backend and a Vue3 fro
* [5010](http://localhost:5010): dev client
* [5020](http://localhost:5020): dev postgres
* [5030](https://localhost:5030): login portal (fake OAuth)
* [5040](https://localhost:5040): dev SMTP server (HTTP port)
* [5041](https://localhost:5041): dev SMTP server (SMTP port)
* [5100](http://localhost:5100): production app
* [5120](http://localhost:5120): production postgres

Expand Down
9 changes: 8 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
version: '3'
name: "fublog-development"

services:

postgres-dev:
Expand All @@ -13,3 +14,9 @@ services:
POSTGRES_DB: "fublog"
ports:
- "5020:5432"

mailhog:
image: mailhog/mailhog:v1.0.1
ports:
- "5041:1025" # SMTP port
- "5040:8025" # HTTP port
12 changes: 12 additions & 0 deletions docker/compose/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ APP_MAIN_WEBSITE_URL=https://fumix.de
# OAUTH_GITLAB_2_ISSUER=
# …

###
### E-Mail settings
###
EMAIL_SMTP_HOST=
EMAIL_SMTP_FROM=
EMAIL_SMTP_USER=
EMAIL_SMTP_PASSWORD=
## Allowed values: "TLS" or "STARTTLS". If not set, defaults to "TLS".
# EMAIL_SMTP_SECURITY=TLS
## Custom SMTP port. If not explicitly set, the default port is 465. Except when `EMAIL_SMTP_SECURITY=STARTTLS` is set, then the default is 587.
# EMAIL_SMTP_PORT=465

###
### As soon as the first user is registered in the blog, you can
### delete everything in this file after this line.
Expand Down
6 changes: 6 additions & 0 deletions server/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
# OAUTH_FAKE_CLIENT_ID=… # Custom client ID (default value is `ID`)
# OAUTH_FAKE_CLIENT_SECRET=… # Custom client secret (default value is `secret`)
# OAUTH_FAKE_ISSUER=localhost:42 # Custom domain (default value is `localhost:5030`)

# For development the user and password can be omitted
EMAIL_SMTP_HOST=localhost
EMAIL_SMTP_FROM=fublog@localhost
EMAIL_SMTP_SECURITY=STARTTLS
EMAIL_SMTP_PORT=5041
87 changes: 87 additions & 0 deletions server/src/settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { AppSettingsDto, asHyperlinkDto, isOAuthType, OAUTH_TYPES, OAuthProvider, OAuthType } from "@fumix/fu-blog-common";
import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import { LeveledLogMethod } from "winston";
import logger from "./logger.js";
import console from "console";
import { readFileSync } from "fs";
import path, { dirname } from "path";
Expand Down Expand Up @@ -50,6 +54,65 @@ export class DatabaseSettings {
static readonly NAME: string = process.env.DATABASE_NAME ?? "fublog";
}

const securityTypes = ["STARTTLS", "TLS"] as const;
type SmtpSecurity = (typeof securityTypes)[number];

function isSmtpSecurity(s: string): s is SmtpSecurity {
return securityTypes.includes(s as SmtpSecurity);
}

export class EmailSettings {
static readonly SMTP_HOST: string | undefined = toNonBlankString(process.env.EMAIL_SMTP_HOST);
static readonly SMTP_FROM: string | undefined = toNonBlankString(process.env.EMAIL_SMTP_FROM);
static readonly SMTP_USER: string | undefined = toNonBlankString(process.env.EMAIL_SMTP_USER);
static readonly SMTP_PASSWORD: string | undefined = toNonBlankString(process.env.EMAIL_SMTP_PASSWORD);
static readonly SMTP_SECURITY: SmtpSecurity | undefined = toEnumValue(
process.env.EMAIL_SMTP_SECURITY,
(it) => (isSmtpSecurity(it) ? it : undefined),
"TLS",
);
static readonly SMTP_PORT: number = toNumberOrDefault(
process.env.EMAIL_SMTP_PORT,
EmailSettings.SMTP_SECURITY === "STARTTLS" ? 587 : 465,
);

static readonly SMTP_OPTIONS: SMTPTransport.Options = {
host: this.SMTP_HOST,
port: this.SMTP_PORT,
secure: this.SMTP_SECURITY !== "STARTTLS",
requireTLS: AppSettings.IS_PRODUCTION,
from: this.SMTP_FROM,
auth:
this.SMTP_USER && this.SMTP_PASSWORD
? {
user: this.SMTP_USER,
pass: this.SMTP_PASSWORD,
}
: undefined,
} as const;

static {
if (!this.SMTP_HOST || !this.SMTP_FROM) {
logger.warn(" πŸ“­ ❓ No email server and/or email from address specified! The blog won't send any email notifications.");
} else {
if (AppSettings.IS_PRODUCTION && (!this.SMTP_USER || !this.SMTP_PASSWORD)) {
logger.warn(
" πŸ“­ πŸ— No username/password set for sending email! Are you sure, that your SMTP server does not need any authentication?",
);
}
createTransport(this.SMTP_OPTIONS).verify(function (error, success) {
const message = error
? " πŸ“­ ❌ Failed to establish SMTP connection for sending e-mails!"
: " πŸ“¬ βœ… Checked that SMTP connection for sending e-mails can be established";
const log: LeveledLogMethod = error ? logger.error : logger.info;
log(
`${message}: ${EmailSettings.SMTP_HOST}:${EmailSettings.SMTP_PORT} (${EmailSettings.SMTP_SECURITY}) ${EmailSettings.SMTP_OPTIONS.auth ? "with username/password" : "with NO AUTHENTICATION!"}`,
);
});
}
}
}

export class ServerSettings {
static readonly API_PATH: string = process.env.SERVER_API_PATH ?? "/api";
static readonly PORT: number = toNumberOrDefault(process.env.SERVER_PORT, 5000);
Expand Down Expand Up @@ -106,9 +169,33 @@ export class OpenAISettings {
static readonly API_KEY: string | undefined = process.env.OPENAI_API_KEY;
}

function toNonBlankString(value: string | undefined | null): string | undefined {
const trimmed = value?.trim();
if ((trimmed?.length ?? 0) > 0) {
return trimmed;
}
}

function toNumberOrDefault(value: string | undefined | null, defaultValue: number): number {
if (value === undefined || value === null) {
return defaultValue;
}
return Number(value);
}

/**
* Converts a string value to an "enum value" (string union type).
*
* @template T the string union type, can include undefined in case the cast function
* or the default value can yield `undefined`
* @param {string | undefined | null} value - The value to be converted.
* @param {(value: string) => T} cast - The function used to cast the value to the enum type.
* @param {T} defaultValue - The default value to be returned if the input value is undefined or null.
* @return {T} - The converted enum value.
*/
function toEnumValue<T extends string | undefined>(value: string | undefined | null, cast: (v: string) => T, defaultValue: T): T {
if (value === undefined || value === null) {
return defaultValue;
}
return cast(value);
}

0 comments on commit 682104b

Please sign in to comment.