Skip to content

Commit

Permalink
Merge branch 'main' into BC-5736-redis-for-tldraw
Browse files Browse the repository at this point in the history
  • Loading branch information
wiaderwek authored Nov 9, 2023
2 parents 962bbc2 + f81f023 commit 24c6150
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 10 deletions.
3 changes: 3 additions & 0 deletions apps/server/src/infra/mail/interfaces/mail-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IMailConfig {
ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: string[];
}
3 changes: 3 additions & 0 deletions apps/server/src/infra/mail/mail.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Module, DynamicModule } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MailService } from './mail.service';
import { IMailConfig } from './interfaces/mail-config';

interface MailModuleOptions {
exchange: string;
Expand All @@ -17,6 +19,7 @@ export class MailModule {
provide: 'MAIL_SERVICE_OPTIONS',
useValue: { exchange: options.exchange, routingKey: options.routingKey },
},
ConfigService<IMailConfig, true>,
],
exports: [MailService],
};
Expand Down
49 changes: 43 additions & 6 deletions apps/server/src/infra/mail/mail.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { createMock } from '@golevelup/ts-jest';
import { ConfigService } from '@nestjs/config';
import { Mail } from './mail.interface';
import { MailService } from './mail.service';
import { IMailConfig } from './interfaces/mail-config';

describe('MailService', () => {
let module: TestingModule;
Expand All @@ -19,6 +22,10 @@ describe('MailService', () => {
MailService,
{ provide: AmqpConnection, useValue: { publish: () => {} } },
{ provide: 'MAIL_SERVICE_OPTIONS', useValue: mailServiceOptions },
{
provide: ConfigService,
useValue: createMock<ConfigService<IMailConfig, true>>({ get: () => ['schul-cloud.org', 'example.com'] }),
},
],
}).compile();

Expand All @@ -34,13 +41,43 @@ describe('MailService', () => {
expect(service).toBeDefined();
});

it('should send given data to queue', async () => {
const data: Mail = { mail: { plainTextContent: 'content', subject: 'Test' }, recipients: ['[email protected]'] };
const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');
describe('send', () => {
describe('when recipients array is empty', () => {
it('should not send email', async () => {
const data: Mail = {
mail: { plainTextContent: 'content', subject: 'Test' },
recipients: ['[email protected]'],
};

await service.send(data);
const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');

const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }];
expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams);
await service.send(data);

expect(amqpConnectionSpy).toHaveBeenCalledTimes(0);
});
});
describe('when sending email', () => {
it('should remove email address that have blacklisted domain and send given data to queue', async () => {
const data: Mail = {
mail: { plainTextContent: 'content', subject: 'Test' },
recipients: ['[email protected]', '[email protected]', '[email protected]', '[email protected]'],
cc: ['[email protected]', '[email protected]', '[email protected]'],
bcc: ['[email protected]', '[email protected]', '[email protected]'],
replyTo: ['[email protected]', '[email protected]', '[email protected]'],
};

const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');

await service.send(data);

expect(data.recipients).toEqual(['[email protected]']);
expect(data.cc).toEqual([]);
expect(data.bcc).toEqual(['[email protected]']);
expect(data.replyTo).toEqual(['[email protected]']);

const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }];
expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams);
});
});
});
});
42 changes: 39 additions & 3 deletions apps/server/src/infra/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Inject, Injectable } from '@nestjs/common';

import { ConfigService } from '@nestjs/config';
import { Mail } from './mail.interface';
import { IMailConfig } from './interfaces/mail-config';

interface MailServiceOptions {
exchange: string;
Expand All @@ -10,12 +11,47 @@ interface MailServiceOptions {

@Injectable()
export class MailService {
private readonly domainBlacklist: string[];

constructor(
private readonly amqpConnection: AmqpConnection,
@Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions
) {}
@Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions,
private readonly configService: ConfigService<IMailConfig, true>
) {
this.domainBlacklist = this.configService.get<string[]>('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS');
}

public async send(data: Mail): Promise<void> {
if (this.domainBlacklist.length > 0) {
data.recipients = this.filterEmailAdresses(data.recipients) as string[];
data.cc = this.filterEmailAdresses(data.cc);
data.bcc = this.filterEmailAdresses(data.bcc);
data.replyTo = this.filterEmailAdresses(data.replyTo);
}

if (data.recipients.length === 0) {
return;
}

await this.amqpConnection.publish(this.options.exchange, this.options.routingKey, data, { persistent: true });
}

private filterEmailAdresses(mails: string[] | undefined): string[] | undefined {
if (mails === undefined || mails === null) {
return mails;
}
const mailWhitelist: string[] = [];

for (const mail of mails) {
const mailDomain = this.getMailDomain(mail);
if (mailDomain && !this.domainBlacklist.includes(mailDomain)) {
mailWhitelist.push(mail);
}
}
return mailWhitelist;
}

private getMailDomain(mail: string): string {
return mail.split('@')[1];
}
}
7 changes: 6 additions & 1 deletion apps/server/src/modules/server/server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { IAccountConfig } from '@modules/account';
import type { IFilesStorageClientConfig } from '@modules/files-storage-client';
import type { IUserConfig } from '@modules/user';
import type { ICommonCartridgeConfig } from '@modules/learnroom/common-cartridge';
import { IMailConfig } from '@src/infra/mail/interfaces/mail-config';

export enum NodeEnvType {
TEST = 'test',
Expand All @@ -19,7 +20,8 @@ export interface IServerConfig
IFilesStorageClientConfig,
IAccountConfig,
IIdentityManagementConfig,
ICommonCartridgeConfig {
ICommonCartridgeConfig,
IMailConfig {
NODE_ENV: string;
SC_DOMAIN: string;
}
Expand All @@ -39,6 +41,9 @@ const config: IServerConfig = {
FEATURE_IDENTITY_MANAGEMENT_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean,
FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean,
FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean,
ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: (Configuration.get('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS') as string)
.split(',')
.map((domain) => domain.trim()),
};

export const serverConfig = () => config;
1 change: 1 addition & 0 deletions config/default.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
},
"ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS": {
"type": "string",
"default":"",
"description": "Add custom domain to the list of blocked domains (comma separated list)."
},
"FEATURE_TSP_AUTO_CONSENT_ENABLED": {
Expand Down

0 comments on commit 24c6150

Please sign in to comment.