Skip to content

Commit 30ab22f

Browse files
feat: add resend provider (#1)
* feat: adding resend provider to deal easily with emails * fix: pnpm lock * fix: check if email was sent or if has some error * fix: redis config for fly * fix: ignore fly env
1 parent de57d05 commit 30ab22f

15 files changed

Lines changed: 208 additions & 8 deletions

File tree

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ APP_SECRET=REPLACE_WITH_LONG_SECRET
77

88
JWT_TOKEN_EXPIRES_IN=30d
99

10-
DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public"
10+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/docmost?schema=public"
1111
REDIS_URL=redis://127.0.0.1:6379
1212

1313
# options: local | s3
@@ -24,7 +24,7 @@ AWS_S3_FORCE_PATH_STYLE=
2424
# default: 50mb
2525
FILE_UPLOAD_SIZE_LIMIT=
2626

27-
# options: smtp | postmark
27+
# options: smtp | postmark | resend
2828
MAIL_DRIVER=smtp
2929
MAIL_FROM_ADDRESS=hello@example.com
3030
MAIL_FROM_NAME=Docmost
@@ -43,4 +43,6 @@ POSTMARK_TOKEN=
4343
# for custom drawio server
4444
DRAWIO_URL=
4545

46-
DISABLE_TELEMETRY=false
46+
# Resend driver config
47+
RESEND_API_TOKEN=
48+
DISABLE_TELEMETRY=false

.env.fly

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# your domain, e.g https://example.com
2+
APP_URL=https://notionish.fly.dev
3+
PORT=3000
4+
5+
# make sure to replace this.
6+
APP_SECRET=DeZy/1Zne6LA3dhWieLdidYLvepXmpSv8p4Sm7GpDBo=
7+
8+
JWT_TOKEN_EXPIRES_IN=30d
9+
10+
DATABASE_URL="postgres://notionish:xrXwVJ6R7PwzaJX@notionish-db.flycast:5432/notionish?sslmode=disable"
11+
REDIS_URL="redis://default:15ac3435cf164abfbaed717546f1e244@fly-notionish-redis.upstash.io:6379?family=6"
12+
13+
# options: local | s3
14+
STORAGE_DRIVER=s3
15+
16+
# S3 driver config
17+
AWS_S3_ACCESS_KEY_ID=tid_SbhaJfkRGDWlEFKIMMvaZ_JMdsOPXxPxFOmZFqSuzwbgQtkfwg
18+
AWS_S3_SECRET_ACCESS_KEY=tsec_FpRz6DyZOmuHhWgTWDCtesOXAkElx9xAGWzxIvTWQIZ2hwprx6C94BahROcdvK3S5UHAIs
19+
AWS_S3_REGION=auto
20+
AWS_S3_BUCKET=notionish-bucket
21+
AWS_S3_ENDPOINT=https://fly.storage.tigris.dev
22+
AWS_S3_FORCE_PATH_STYLE=
23+
24+
# default: 50mb
25+
FILE_UPLOAD_SIZE_LIMIT=
26+
27+
# options: smtp | postmark | resend
28+
MAIL_DRIVER=resend
29+
MAIL_FROM_ADDRESS=contact@iagocavalcante.com
30+
MAIL_FROM_NAME=Notionish
31+
32+
# Postmark driver config
33+
POSTMARK_TOKEN=
34+
35+
# Resend driver config
36+
RESEND_API_TOKEN=re_GZ1henng_6N8F5Hm5JqkE2gPzVe92ZfGw

.github/workflows/fly-deploy.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
2+
3+
name: Fly Deploy
4+
on:
5+
push:
6+
branches:
7+
- main
8+
jobs:
9+
deploy:
10+
name: Deploy app
11+
runs-on: ubuntu-latest
12+
concurrency: deploy-group # optional: ensure only one action runs at a time
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: superfly/flyctl-actions/setup-flyctl@master
16+
- run: flyctl deploy --remote-only
17+
env:
18+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.env
22
.env.dev
33
.env.prod
4+
.env.fly
45
data
56
# compiled output
67
/dist

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodejs 22.6.0

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"postmark": "^4.0.5",
7878
"react": "^18.3.1",
7979
"reflect-metadata": "^0.2.2",
80+
"resend": "^4.0.1",
8081
"rxjs": "^7.8.1",
8182
"sanitize-filename-ts": "^1.0.2",
8283
"socket.io": "^4.8.1",

apps/server/src/integrations/environment/environment.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ export class EnvironmentService {
145145
return this.configService.get<string>('DRAWIO_URL');
146146
}
147147

148+
getResendApiToken(): string {
149+
return this.configService.get<string>('RESEND_API_TOKEN');
150+
}
151+
148152
isCloud(): boolean {
149153
const cloudConfig = this.configService
150154
.get<string>('CLOUD', 'false')

apps/server/src/integrations/environment/environment.validation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class EnvironmentVariables {
4343
APP_SECRET: string;
4444

4545
@IsOptional()
46-
@IsIn(['smtp', 'postmark'])
46+
@IsIn(['smtp', 'postmark', 'resend'])
4747
MAIL_DRIVER: string;
4848

4949
@IsOptional()

apps/server/src/integrations/health/redis.health.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { Injectable, Logger } from '@nestjs/common';
66
import { EnvironmentService } from '../environment/environment.service';
77
import { Redis } from 'ioredis';
8+
import { parseRedisUrl } from 'src/common/helpers/utils';
89

910
@Injectable()
1011
export class RedisHealthIndicator {
@@ -19,8 +20,16 @@ export class RedisHealthIndicator {
1920
const indicator = this.healthIndicatorService.check(key);
2021

2122
try {
22-
const redis = new Redis(this.environmentService.getRedisUrl(), {
23+
const { host, port, password, db } = parseRedisUrl(
24+
this.environmentService.getRedisUrl(),
25+
);
26+
const redis = new Redis({
27+
host,
28+
port,
29+
password,
30+
db,
2331
maxRetriesPerRequest: 15,
32+
family: 6,
2433
});
2534

2635
await redis.ping();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { MailDriver } from './interfaces/mail-driver.interface';
2+
import { ResendConfig } from '../interfaces';
3+
import { Resend } from 'resend';
4+
import { MailMessage } from '../interfaces/mail.message';
5+
import { Logger } from '@nestjs/common';
6+
import { mailLogName } from '../mail.utils';
7+
8+
export class ResendDriver implements MailDriver {
9+
private readonly logger = new Logger(mailLogName(ResendDriver.name));
10+
private readonly resendClient: Resend;
11+
12+
constructor(config: ResendConfig) {
13+
this.logger.debug(`TOKEN ${config.resendApiToken}`);
14+
this.resendClient = new Resend(config.resendApiToken);
15+
}
16+
17+
private formatEmailAddress(email: string): string {
18+
if (email.includes('<') && email.includes('>')) {
19+
const matches = email.match(/<(.+)>/);
20+
if (matches && matches[1]) {
21+
return matches[1];
22+
}
23+
}
24+
return email;
25+
}
26+
27+
async sendMail(message: MailMessage): Promise<void> {
28+
try {
29+
const fromEmail = this.formatEmailAddress(message.from);
30+
this.logger.debug(`Attempting to send mail from ${fromEmail}`);
31+
32+
const { data, error } = await this.resendClient.emails.send({
33+
from: fromEmail,
34+
to: message.to,
35+
subject: message.subject,
36+
text: message.text,
37+
html: message.html,
38+
});
39+
40+
if (error) {
41+
this.logger.error(`Failed to send mail: ${error.message}`);
42+
throw new Error(error.message);
43+
}
44+
45+
this.logger.debug(`Email sent successfully. ID: ${data?.id}`);
46+
} catch (err) {
47+
this.logger.error(`Failed to send mail: ${err}`);
48+
throw err;
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)