-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: emails #10
feat: emails #10
Changes from 10 commits
b32de93
e279bc3
c9e7c42
7f50fd2
48e42c6
c65b1ba
46a9c29
90cd7ce
c2a380a
8621ad8
5731c84
0e6fe0f
7f031e5
8d1dcdc
50282dc
7e9485e
5c6fc6b
086c024
c17047e
40be8e9
af86c98
fc3696c
31bafb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,22 @@ | ||
# GENERAL | ||
CORS_ORIGIN="https://app.guidebook.localhost" | ||
EMAIL_ADAPTER="mailhog" | ||
|
||
# DATABASE | ||
DATABASE_URL="postgres://postgres:guidebook@localhost:5432/guidebook" | ||
|
||
# JWT | ||
JWT_SECRET= | ||
JWT_REFRESH_SECRET= | ||
CORS_ORIGIN= | ||
JWT_EXPIRATION_TIME= | ||
|
||
# MAILS | ||
SMTP_HOST= | ||
SMTP_PORT= | ||
SMTP_USER= | ||
SMTP_PASSWORD= | ||
|
||
# AWS | ||
AWS_REGION= | ||
AWS_ACCESS_KEY_ID= | ||
AWS_SECRET_ACCESS_KEY= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { registerAs } from "@nestjs/config"; | ||
import { Static, Type } from "@sinclair/typebox"; | ||
import { Value } from "@sinclair/typebox/value"; | ||
|
||
const schema = Type.Object({ | ||
AWS_REGION: Type.String(), | ||
AWS_ACCESS_KEY_ID: Type.String(), | ||
AWS_SECRET_ACCESS_KEY: Type.String(), | ||
}); | ||
|
||
type AWSConfigSchema = Static<typeof schema>; | ||
|
||
export default registerAs("aws", (): AWSConfigSchema => { | ||
const values = { | ||
AWS_REGION: process.env.AWS_REGION, | ||
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, | ||
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, | ||
}; | ||
|
||
return Value.Decode(schema, values); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { registerAs } from "@nestjs/config"; | ||
import { Static, Type } from "@sinclair/typebox"; | ||
import { Value } from "@sinclair/typebox/value"; | ||
|
||
const schema = Type.Object({ | ||
SMTP_HOST: Type.String(), | ||
SMTP_PORT: Type.Number(), | ||
SMTP_USER: Type.String(), | ||
SMTP_PASSWORD: Type.String(), | ||
USE_MAILHOG: Type.Boolean(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still use this one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup, in smtp adapter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. referring to previous comment
then we just need to create a new adapter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should require environments based on what we want to use, eg I am going with local one I don't need to setup aws. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so it works like that - it checks what adapter we have specified in env There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but I may not understand something 😅 |
||
EMAIL_ADAPTER: Type.Union([ | ||
Type.Literal("mailhog"), | ||
Type.Literal("smtp"), | ||
Type.Literal("ses"), | ||
]), | ||
}); | ||
|
||
export type EmailConfigSchema = Static<typeof schema>; | ||
|
||
export default registerAs("email", (): EmailConfigSchema => { | ||
const values = { | ||
SMTP_HOST: process.env.SMTP_HOST, | ||
SMTP_PORT: parseInt(process.env.SMTP_PORT ?? "465", 10), | ||
SMTP_USER: process.env.SMTP_USER, | ||
SMTP_PASSWORD: process.env.SMTP_PASSWORD, | ||
USE_MAILHOG: process.env.USE_MAILHOG === "true", | ||
EMAIL_ADAPTER: process.env.EMAIL_ADAPTER, | ||
}; | ||
|
||
return Value.Decode(schema, values); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Email } from "../email.interface"; | ||
|
||
export abstract class EmailAdapter { | ||
abstract sendMail(email: Email): Promise<void>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Injectable } from "@nestjs/common"; | ||
import * as nodemailer from "nodemailer"; | ||
import { Email } from "../email.interface"; | ||
import { EmailAdapter } from "./email.adapter"; | ||
|
||
@Injectable() | ||
export class LocalAdapter extends EmailAdapter { | ||
private transporter: nodemailer.Transporter; | ||
|
||
constructor() { | ||
super(); | ||
this.transporter = nodemailer.createTransport({ | ||
host: "localhost", | ||
port: 1025, | ||
ignoreTLS: true, | ||
}); | ||
} | ||
|
||
async sendMail(email: Email): Promise<void> { | ||
await this.transporter.sendMail(email); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Injectable } from "@nestjs/common"; | ||
import { ConfigService } from "@nestjs/config"; | ||
import * as nodemailer from "nodemailer"; | ||
import { Email } from "../email.interface"; | ||
import { EmailAdapter } from "./email.adapter"; | ||
|
||
@Injectable() | ||
export class NodemailerAdapter extends EmailAdapter { | ||
private transporter: nodemailer.Transporter; | ||
|
||
constructor(private configService: ConfigService) { | ||
super(); | ||
this.transporter = nodemailer.createTransport(this.getNodemailerOptions()); | ||
} | ||
|
||
async sendMail(email: Email): Promise<void> { | ||
await this.transporter.sendMail(email); | ||
} | ||
|
||
private getNodemailerOptions() { | ||
return { | ||
host: this.configService.get<string>("email.SMTP_HOST"), | ||
port: this.configService.get<number>("email.SMTP_PORT"), | ||
secure: true, | ||
auth: { | ||
user: this.configService.get<string>("email.SMTP_USER"), | ||
pass: this.configService.get<string>("email.SMTP_PASSWORD"), | ||
}, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Injectable } from "@nestjs/common"; | ||
import { ConfigService } from "@nestjs/config"; | ||
import { SES, SESClientConfig } from "@aws-sdk/client-ses"; | ||
import { EmailAdapter } from "./email.adapter"; | ||
import { Email } from "../email.interface"; | ||
|
||
@Injectable() | ||
export class AWSSESAdapter extends EmailAdapter { | ||
private ses: SES; | ||
|
||
constructor(private configService: ConfigService) { | ||
super(); | ||
const config: SESClientConfig = this.getAWSConfig(); | ||
this.ses = new SES(config); | ||
} | ||
|
||
private getAWSConfig(): SESClientConfig { | ||
const region = this.configService.get<string>("aws.AWS_REGION"); | ||
const accessKeyId = this.configService.get<string>("aws.AWS_ACCESS_KEY_ID"); | ||
const secretAccessKey = this.configService.get<string>( | ||
"aws.AWS_SECRET_ACCESS_KEY", | ||
); | ||
|
||
if (!region || !accessKeyId || !secretAccessKey) { | ||
throw new Error("Missing AWS configuration"); | ||
} | ||
|
||
return { | ||
region, | ||
credentials: { | ||
accessKeyId, | ||
secretAccessKey, | ||
}, | ||
}; | ||
} | ||
|
||
async sendMail(email: Email): Promise<void> { | ||
const params = { | ||
Source: email.from, | ||
Destination: { | ||
ToAddresses: [email.to], | ||
}, | ||
Message: { | ||
Subject: { | ||
Data: email.subject, | ||
}, | ||
Body: { | ||
Text: { | ||
Data: email.text, | ||
}, | ||
Html: { | ||
Data: email.html, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
try { | ||
await this.ses.sendEmail(params); | ||
} catch (error) { | ||
console.error("Error sending email via AWS SES:", error); | ||
throw error; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface Email { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this interface it is possible to pas an Email without both text and html. Can we make it that we need at least one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done ✅ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I specify email with only text, the html property has
|
||
to: string; | ||
from: string; | ||
subject: string; | ||
text?: string; | ||
html?: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to create EmailTestingAdapter which would get all messages and save them to array/object and provides methods like getAllEmail/lastEmail etc. this way we can easily check in tests what emails were sent and assert them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have EmailTestingAdapter, you don't need to mock it, inject it in the test application as an adapter, then use DI to work with it.