Skip to content

Commit e136bef

Browse files
authored
feat: RU mail account (#3526)
* feat: RU mail account * fix: vulnerability * fix: improved comments * feat: custom mail account only for user mails * chore: cleanup * chore: removed log
1 parent 9bc1365 commit e136bef

11 files changed

Lines changed: 741 additions & 633 deletions

File tree

package-lock.json

Lines changed: 644 additions & 380 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config/config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,23 @@ export class Configuration {
620620
liqMail: process.env.LIQ_MAIL || 'liq@dfx.swiss',
621621
noReplyMail: process.env.NOREPLY_MAIL || 'noreply@dfx.swiss',
622622
},
623+
wallet: {
624+
onchainlabs: {
625+
template: 'onChainLabs',
626+
},
627+
...(process.env.REALUNIT_MAIL_USER && {
628+
RealUnit: {
629+
host: 'mail.infomaniak.com',
630+
port: 587,
631+
secure: false,
632+
user: process.env.REALUNIT_MAIL_USER,
633+
pass: process.env.REALUNIT_MAIL_PASS,
634+
fromAddress: process.env.REALUNIT_MAIL_USER,
635+
displayName: 'RealUnit',
636+
template: 'user-v2',
637+
},
638+
}),
639+
},
623640
};
624641

625642
coinGecko = {

src/subdomains/supporting/notification/entities/mail/base/mail.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GetConfig } from 'src/config/config';
1+
import { Config } from 'src/config/config';
22
import { Notification, NotificationOptions } from '../../notification.entity';
33

44
export interface MailParamBase {
@@ -9,6 +9,7 @@ export interface MailParamBase {
99
cc?: string;
1010
bcc?: string;
1111
template?: string;
12+
walletName?: string;
1213
options?: NotificationOptions;
1314
correlationId?: string;
1415
}
@@ -31,29 +32,30 @@ export interface MailParamsNew extends MailParamBase {
3132
}
3233

3334
export class Mail extends Notification {
34-
readonly #from: { name: string; address: string } = {
35-
name: 'DFX.swiss',
36-
address: GetConfig().mail.contact.noReplyMail,
37-
};
35+
readonly #from: { name: string; address: string };
3836
readonly #to: string | string[];
3937
readonly #cc: string;
4038
readonly #bcc: string;
4139
readonly #subject: string;
42-
readonly #template: string = GetConfig().mail.defaultMailTemplate;
40+
readonly #template: string;
4341
readonly #templateParams: { [name: string]: any };
42+
readonly #walletName?: string;
4443

4544
constructor(params: MailParams | MailParamsNew) {
4645
super();
4746

47+
const walletMailConfig = params.walletName ? Config.mail.wallet[params.walletName] : undefined;
48+
49+
this.#walletName = params.walletName;
4850
this.#to = params.to;
4951
this.#subject = params.subject;
5052
this.#from = {
51-
name: params.displayName ?? 'DFX.swiss',
52-
address: params.from ?? GetConfig().mail.contact.noReplyMail,
53+
name: params.displayName ?? walletMailConfig?.displayName ?? 'DFX.swiss',
54+
address: params.from ?? walletMailConfig?.fromAddress ?? Config.mail.contact.noReplyMail,
5355
};
54-
this.#cc = params.cc ?? this.#cc;
55-
this.#bcc = params.bcc ?? this.#bcc;
56-
this.#template = params.template ?? this.#template;
56+
this.#cc = params.cc;
57+
this.#bcc = params.bcc;
58+
this.#template = params.template ?? Config.mail.defaultMailTemplate;
5759
this.#templateParams = params.templateParams;
5860
}
5961

@@ -85,4 +87,8 @@ export class Mail extends Notification {
8587
get subject(): string {
8688
return this.#subject;
8789
}
90+
91+
get walletName(): string | undefined {
92+
return this.#walletName;
93+
}
8894
}

src/subdomains/supporting/notification/entities/mail/user-mail-v2.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ export class UserMailV2 extends Mail {
3535
instagramUrl: Config.social.instagram,
3636
};
3737

38+
const walletMailConfig = wallet?.name ? Config.mail.wallet[wallet.name] : undefined;
39+
3840
super({
3941
...params,
40-
template: wallet?.name === 'onchainlabs' ? 'onChainLabs' : 'user-v2',
42+
walletName: wallet?.name,
43+
template: walletMailConfig?.template ?? 'user-v2',
4144
templateParams: { ...defaultParams, ...params },
4245
});
4346
}

src/subdomains/supporting/notification/entities/mail/user-mail.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/subdomains/supporting/notification/enums/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export enum NotificationType {
55
export enum MailType {
66
GENERIC = 'Generic',
77
ERROR_MONITORING = 'ErrorMonitoring',
8-
USER_DEPRECATED = 'UserDeprecated',
98
USER_V2 = 'UserV2',
109
PERSONAL = 'Personal',
1110
INTERNAL = 'Internal',

src/subdomains/supporting/notification/factories/mail.factory.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Mail, MailParams } from '../entities/mail/base/mail';
77
import { ErrorMonitoringMail, ErrorMonitoringMailInput } from '../entities/mail/error-monitoring-mail';
88
import { InternalMail, MailRequestInternalInput } from '../entities/mail/internal-mail';
99
import { MailRequestPersonalInput, PersonalMail } from '../entities/mail/personal-mail';
10-
import { MailRequestUserInput, UserMail, UserMailTable } from '../entities/mail/user-mail';
1110
import { MailRequestUserInputV2, UserMailV2 } from '../entities/mail/user-mail-v2';
1211
import { MailContext, MailContextType, MailContextTypeMapper, MailType } from '../enums';
1312
import { MailAffix, MailRequest, MailRequestGenericInput, TranslationItem, TranslationParams } from '../interfaces';
@@ -82,10 +81,6 @@ export class MailFactory {
8281
return this.createErrorMonitoringMail(request);
8382
}
8483

85-
case MailType.USER_DEPRECATED: {
86-
return this.createUserMail(request);
87-
}
88-
8984
case MailType.USER_V2: {
9085
return this.createUserV2Mail(request);
9186
}
@@ -146,27 +141,6 @@ export class MailFactory {
146141
return new ErrorMonitoringMail({ subject, errors, correlationId, options });
147142
}
148143

149-
private createUserMail(request: MailRequest): UserMail {
150-
const { correlationId, options } = request;
151-
const { userData, wallet, title, salutation, prefix, suffix, table } = request.input as MailRequestUserInput;
152-
153-
const lang = userData.language.symbol;
154-
155-
return new UserMail(
156-
{
157-
to: userData.mail,
158-
subject: this.translate(title, lang),
159-
salutation: salutation && this.translate(salutation.key, lang, salutation.params),
160-
prefix: prefix && this.getMailAffix(prefix, lang),
161-
table: table && this.getTable(table, lang),
162-
suffix: suffix && this.getMailAffix(suffix, lang),
163-
correlationId,
164-
options,
165-
},
166-
wallet,
167-
);
168-
}
169-
170144
private createUserV2Mail(request: MailRequest): UserMailV2 {
171145
const { correlationId, options, context } = request;
172146
const { userData, wallet, title, salutation, texts } = request.input as MailRequestUserInputV2;
@@ -227,13 +201,6 @@ export class MailFactory {
227201
);
228202
}
229203

230-
private getTable(table: Record<string, string>, lang: string): UserMailTable[] {
231-
return Object.entries(Util.removeNullFields(table)).map(([key, value]) => ({
232-
text: this.translate(key, lang),
233-
value: value,
234-
}));
235-
}
236-
237204
private getMailAffix(affix: TranslationItem[], lang = 'en'): MailAffix[] {
238205
return affix
239206
.filter((i) => i)

src/subdomains/supporting/notification/interfaces/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ErrorMonitoringMailInput } from '../entities/mail/error-monitoring-mail';
22
import { MailRequestInternalInput } from '../entities/mail/internal-mail';
33
import { MailRequestPersonalInput } from '../entities/mail/personal-mail';
4-
import { MailRequestUserInput } from '../entities/mail/user-mail';
54
import { MailRequestUserInputV2 } from '../entities/mail/user-mail-v2';
65
import { NotificationOptions } from '../entities/notification.entity';
76
import { MailContext, MailType } from '../enums';
@@ -12,7 +11,6 @@ export interface MailRequest {
1211
input:
1312
| MailRequestGenericInput
1413
| ErrorMonitoringMailInput
15-
| MailRequestUserInput
1614
| MailRequestUserInputV2
1715
| MailRequestPersonalInput
1816
| MailRequestInternalInput;

src/subdomains/supporting/notification/notification.module.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { MailerModule } from '@nestjs-modules/mailer';
21
import { Module } from '@nestjs/common';
32
import { TypeOrmModule } from '@nestjs/typeorm';
4-
import { GetConfig } from 'src/config/config';
53
import { SharedModule } from 'src/shared/shared.module';
64
import { Notification } from './entities/notification.entity';
75
import { MailFactory } from './factories/mail.factory';
@@ -12,7 +10,7 @@ import { NotificationJobService } from './services/notification-job.service';
1210
import { NotificationService } from './services/notification.service';
1311

1412
@Module({
15-
imports: [TypeOrmModule.forFeature([Notification]), MailerModule.forRoot(GetConfig().mail.options), SharedModule],
13+
imports: [TypeOrmModule.forFeature([Notification]), SharedModule],
1614
providers: [NotificationRepository, MailService, NotificationService, MailFactory, NotificationJobService],
1715
controllers: [NotificationController],
1816
exports: [NotificationService, MailFactory],
Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1-
import { MailerOptions, MailerService } from '@nestjs-modules/mailer';
1+
import { MailerOptions } from '@nestjs-modules/mailer';
22
import { Injectable } from '@nestjs/common';
3+
import * as fs from 'fs';
4+
import * as handlebars from 'handlebars';
5+
import * as nodemailer from 'nodemailer';
6+
import { join } from 'path';
37
import { Config, Environment } from 'src/config/config';
48
import { DfxLogger } from 'src/shared/services/dfx-logger';
59
import { Mail } from '../entities/mail/base/mail';
610

11+
// custom wallet config for UserMailV2
12+
export interface WalletMailConfig {
13+
host: string;
14+
port: number;
15+
secure: boolean; // true for 465, false for STARTTLS on 587
16+
user: string;
17+
pass: string;
18+
fromAddress: string;
19+
displayName: string;
20+
template: string;
21+
}
22+
723
export interface MailOptions {
824
options: MailerOptions;
925
defaultMailTemplate: string;
@@ -13,30 +29,31 @@ export interface MailOptions {
1329
liqMail: string;
1430
noReplyMail: string;
1531
};
32+
wallet: Record<string, Partial<WalletMailConfig>>;
1633
}
1734

1835
@Injectable()
1936
export class MailService {
2037
private readonly logger = new DfxLogger(MailService);
21-
22-
constructor(private readonly mailerService: MailerService) {}
38+
private readonly transports = new Map<string, nodemailer.Transporter>();
2339

2440
async send(mail: Mail): Promise<void> {
25-
// Skip mail sending in local environment
2641
if (Config.environment === Environment.LOC) {
2742
this.logger.info(`[LOCAL DEV] Mail skipped - to: ${mail.to}, subject: ${mail.subject}`);
2843
return;
2944
}
3045

3146
try {
32-
await this.mailerService.sendMail({
33-
from: mail.from,
47+
const transport = this.getTransport(mail.walletName);
48+
const html = this.compileTemplate(mail.template, mail.templateParams);
49+
50+
await transport.sendMail({
51+
from: { name: mail.from.name, address: mail.from.address },
3452
to: mail.to,
3553
cc: mail.cc,
3654
bcc: mail.bcc,
3755
subject: mail.subject,
38-
template: mail.template,
39-
context: mail.templateParams,
56+
html,
4057
});
4158
} catch (e) {
4259
this.logger.error(
@@ -46,4 +63,37 @@ export class MailService {
4663
throw e;
4764
}
4865
}
66+
67+
private getTransport(walletName?: string): nodemailer.Transporter {
68+
const walletConfig = walletName ? Config.mail.wallet[walletName] : undefined;
69+
const key = walletConfig?.host ? walletName : 'default';
70+
71+
let transport = this.transports.get(key);
72+
if (!transport) {
73+
transport = this.createTransport(walletConfig);
74+
this.transports.set(key, transport);
75+
}
76+
77+
return transport;
78+
}
79+
80+
private createTransport(walletConfig?: Partial<WalletMailConfig>): nodemailer.Transporter {
81+
if (walletConfig?.host) {
82+
return nodemailer.createTransport({
83+
host: walletConfig.host,
84+
port: walletConfig.port,
85+
secure: walletConfig.secure,
86+
auth: { user: walletConfig.user, pass: walletConfig.pass },
87+
tls: { rejectUnauthorized: false },
88+
});
89+
}
90+
91+
return nodemailer.createTransport(Config.mail.options.transport as nodemailer.TransportOptions);
92+
}
93+
94+
private compileTemplate(template: string, params: Record<string, unknown>): string {
95+
const templatePath = join(Config.mail.options.template.dir, `${template}.hbs`);
96+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
97+
return handlebars.compile(templateContent)(params);
98+
}
4999
}

0 commit comments

Comments
 (0)