-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmailer.service.ts
More file actions
105 lines (89 loc) · 2.8 KB
/
Copy pathmailer.service.ts
File metadata and controls
105 lines (89 loc) · 2.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { Injectable } from '@nestjs/common';
import { readFile } from 'fs/promises';
import { resolve } from 'path';
import {
ISendMailOptions,
MailerService as MailerService_,
} from '@nestjs-modules/mailer';
import { InjectRedis } from '@songkeys/nestjs-redis';
import Redis from 'ioredis';
import { ThrottlerException } from '@nestjs/throttler';
@Injectable()
export class MailerService {
private readonly TEMPLATE_PATH = resolve(__dirname, 'templates');
private templates: Record<string, string> = {}; // cache templates
private readonly MAILER_FROM = `Crossbell <${process.env.MAILER_USER}>`;
readonly AVAILABLE_TEMPLATES = ['signup', 'reset-password'] as const;
constructor(
private readonly mailer: MailerService_,
@InjectRedis() private readonly redis: Redis,
) {}
async sendSignupEmail(email: string, code: string) {
const template = await this.getTemplate('signup');
const html = template
.replace(/{{code}}/g, code)
.replace(/{{email}}/g, email.split('@')[0]);
return this.sendMail({
to: email,
subject: '🛎 Welcome to Crossbell - Verify your email',
html,
});
}
async sendResetPasswordEmail(email: string, code: string) {
const template = await this.getTemplate('reset-password');
const html = template
.replace(/{{code}}/g, code)
.replace(/{{email}}/g, email.split('@')[0]);
return this.sendMail({
to: email,
subject: '🛎 Crossbell - Reset your password',
html,
});
}
private async getTemplate(
name: (typeof this)['AVAILABLE_TEMPLATES'][number],
) {
if (this.templates[name]) {
return this.templates[name];
}
const template = await readFile(
resolve(this.TEMPLATE_PATH, name, `prod.html`),
'utf-8',
);
this.templates[name] = template;
return template;
}
private async sendMail(options: ISendMailOptions) {
// throttle checker
let to = options.to ?? [];
if (!Array.isArray(to)) {
to = [to];
}
const toAddresses = to.map<string>((addr) => {
if (typeof addr === 'string') {
return addr;
} else {
return addr.address;
}
});
const throttled = await Promise.all(
toAddresses.map((addr) => this.isMailThrottled(addr)),
);
if (throttled.some((throttled) => throttled)) {
throw new ThrottlerException("You're sending too many emails");
}
return this.mailer.sendMail({ from: this.MAILER_FROM, ...options });
}
private async isMailThrottled(
address: string,
seconds: number = 60,
limit: number = 2,
): Promise<boolean> {
const cacheKey = `mail:throttle:${address}:${this.MAILER_FROM}`;
const count = await this.redis.incr(cacheKey);
if (count === 1) {
await this.redis.expire(cacheKey, seconds);
}
return count > limit;
}
}