email: add EmailService SMTP layer (Nodemailer, swappable EmailSender interface)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
debaeb2b6f
commit
e11a434725
|
|
@ -13,13 +13,15 @@
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"markdown-it": "^14.3.0"
|
"markdown-it": "^14.3.0",
|
||||||
|
"nodemailer": "^9.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/express-session": "^1.17.10",
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/nodemailer": "^8.0.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +232,16 @@
|
||||||
"undici-types": "~6.21.0"
|
"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": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
||||||
|
|
@ -1024,6 +1036,15 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"markdown-it": "^14.3.0"
|
"markdown-it": "^14.3.0",
|
||||||
|
"nodemailer": "^9.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/express-session": "^1.17.10",
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/nodemailer": "^8.0.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface EmailSender
|
||||||
|
{
|
||||||
|
sendEmail( to: string, subject: string, body: string ): Promise<void>;
|
||||||
|
}
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -95,6 +95,26 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3><a href="2026/07-July/12-Sunday/index.html">Sunday, 12 July 2026</a></h3>
|
<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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@
|
||||||
conversation session in memory.
|
conversation session in memory.
|
||||||
A reusable <code><confirm-dialog></code> component replaces browser
|
A reusable <code><confirm-dialog></code> component replaces browser
|
||||||
<code>confirm()</code> for destructive actions (currently project deletion).
|
<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>
|
||||||
<p style="margin-top:0.75rem">
|
<p style="margin-top:0.75rem">
|
||||||
The app is deployed and publicly accessible at
|
The app is deployed and publicly accessible at
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue