email: add EmailService SMTP layer (Nodemailer, swappable EmailSender interface)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rokojori 2026-07-12 18:50:04 +02:00
parent debaeb2b6f
commit e11a434725
8 changed files with 119 additions and 3 deletions

23
package-lock.json generated
View File

@ -13,13 +13,15 @@
"bcryptjs": "^2.4.3",
"express": "^4.18.2",
"express-session": "^1.17.3",
"markdown-it": "^14.3.0"
"markdown-it": "^14.3.0",
"nodemailer": "^9.0.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/express-session": "^1.17.10",
"@types/node": "^20.11.0",
"@types/nodemailer": "^8.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
@ -230,6 +232,16 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/nodemailer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
@ -1024,6 +1036,15 @@
"node": ">= 0.6"
}
},
"node_modules/nodemailer": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@ -12,13 +12,15 @@
"bcryptjs": "^2.4.3",
"express": "^4.18.2",
"express-session": "^1.17.3",
"markdown-it": "^14.3.0"
"markdown-it": "^14.3.0",
"nodemailer": "^9.0.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/express-session": "^1.17.10",
"@types/node": "^20.11.0",
"@types/nodemailer": "^8.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}

View File

@ -0,0 +1,4 @@
export interface EmailSender
{
sendEmail( to: string, subject: string, body: string ): Promise<void>;
}

View File

@ -0,0 +1,20 @@
import { EmailSender } from './EmailSender';
import { SMTPEmailSender } from './SMTPEmailSender';
export class EmailService
{
private static sender: EmailSender = new SMTPEmailSender(
{
host: process.env.SMTP_HOST ?? '',
port: Number( process.env.SMTP_PORT ?? 587 ),
secure: process.env.SMTP_SECURE === 'true',
user: process.env.SMTP_USER ?? '',
pass: process.env.SMTP_PASS ?? '',
from: process.env.SMTP_FROM ?? ''
} );
static async sendEmail( to: string, subject: string, body: string ): Promise<void>
{
return EmailService.sender.sendEmail( to, subject, body );
}
}

View File

@ -0,0 +1,45 @@
import nodemailer from 'nodemailer';
import { EmailSender } from './EmailSender';
export interface SMTPConfig
{
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
from: string;
}
export class SMTPEmailSender implements EmailSender
{
private transporter: nodemailer.Transporter;
private from: string;
constructor( config: SMTPConfig )
{
this.from = config.from;
this.transporter = nodemailer.createTransport(
{
host: config.host,
port: config.port,
secure: config.secure,
auth:
{
user: config.user,
pass: config.pass
}
} );
}
async sendEmail( to: string, subject: string, body: string ): Promise<void>
{
await this.transporter.sendMail(
{
from: this.from,
to,
subject,
text: body
} );
}
}

View File

@ -95,6 +95,26 @@
</div>
</div>
<div class="card">
<h3>EmailService — SMTP email sending</h3>
<p>
Added a thin email layer to the backend under
<code>source/server/email/</code>: an <code>EmailSender</code> interface,
an <code>SMTPEmailSender</code> implementation using Nodemailer, and an
<code>EmailService</code> static facade that instantiates the sender from
environment variables (<code>SMTP_HOST</code>, <code>SMTP_PORT</code>,
<code>SMTP_SECURE</code>, <code>SMTP_USER</code>, <code>SMTP_PASS</code>,
<code>SMTP_FROM</code>). Swapping the implementation requires changing one
line in <code>EmailService.ts</code>.
</p>
<div class="tags">
<span class="tag">Nodemailer</span>
<span class="tag">EmailSender interface</span>
<span class="tag">static facade</span>
<span class="tag">env vars</span>
</div>
</div>
</section>
<section>

View File

@ -21,7 +21,7 @@
<div class="card">
<h3><a href="2026/07-July/12-Sunday/index.html">Sunday, 12 July 2026</a></h3>
<p>Full directory restructure and first production deployment — source/, build/, source/pages/; Roject live at roject.rokojori.com via nginx + systemd.</p>
<p>Full directory restructure and first production deployment — source/, build/, source/pages/; Roject live at roject.rokojori.com via nginx + systemd. EmailService SMTP layer added.</p>
</div>
<div class="card">

View File

@ -70,6 +70,10 @@
conversation session in memory.
A reusable <code>&lt;confirm-dialog&gt;</code> component replaces browser
<code>confirm()</code> for destructive actions (currently project deletion).
An <code>EmailService</code> static facade (<code>source/server/email/</code>)
provides a single <code>sendEmail()</code> entry point backed by a swappable
<code>EmailSender</code> interface; the default implementation is
<code>SMTPEmailSender</code> (Nodemailer), configured via environment variables.
</p>
<p style="margin-top:0.75rem">
The app is deployed and publicly accessible at