Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/modules/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ import { DomainRepository } from './repositories/domain.repository.js';
DomainRepository,
AccountService,
],
exports: [AccountService, DomainRepository],
exports: [AccountService],
})
export class AccountModule {}
5 changes: 5 additions & 0 deletions src/modules/account/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@nestjs/common';
import { AccountProvider } from './account-provider.port.js';
import { MailAccount } from './domain/mail-account.domain.js';
import { MailDomain } from './domain/mail-domain.domain.js';
import { AccountRepository } from './repositories/account.repository.js';
import { AddressRepository } from './repositories/address.repository.js';
import { DomainRepository } from './repositories/domain.repository.js';
Expand All @@ -27,6 +28,10 @@ export class AccountService {
return this.getAccountOrFail(userId);
}

async listActiveDomains(): Promise<MailDomain[]> {
return this.domains.findAllActive();
}

async findAccount(userId: string): Promise<MailAccount | null> {
return this.accounts.findByUserId(userId);
}
Expand Down
32 changes: 25 additions & 7 deletions src/modules/email/email.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,45 +38,63 @@ describe('EmailController', () => {
});

describe('list', () => {
it('When list is called with no query params, then it uses defaults', async () => {
it('when called with no query params, then it lists all emails', async () => {
const response = {
emails: [newEmailSummary()],
total: 1,
hasMoreMails: false,
};
emailService.listEmails.mockResolvedValue(response);

const result = await controller.list(userEmail, 'inbox');
const result = await controller.list(userEmail);

expect(emailService.listEmails).toHaveBeenCalledWith(
userEmail,
'inbox',
undefined,
20,
0,
undefined,
);
expect(result).toBe(response);
});

it('When list is called with limit and position, then it parses them', async () => {
it('when called with a mailbox filter, then it filters by mailbox', async () => {
emailService.listEmails.mockResolvedValue({
emails: [],
total: 0,
hasMoreMails: false,
});

await controller.list(userEmail, 'sent', '10', '5');
await controller.list(userEmail, 'inbox');

expect(emailService.listEmails).toHaveBeenCalledWith(
userEmail,
'inbox',
20,
0,
undefined,
);
});

it('when called with limit, position and anchorId, then it parses them', async () => {
emailService.listEmails.mockResolvedValue({
emails: [],
total: 0,
hasMoreMails: false,
});

await controller.list(userEmail, 'sent', '10', '5', 'Ma1f09b');

expect(emailService.listEmails).toHaveBeenCalledWith(
userEmail,
'sent',
10,
5,
undefined,
'Ma1f09b',
);
});

it('When list is called with non-numeric strings, then it falls back to defaults', async () => {
it('when called with non-numeric strings, then it falls back to defaults', async () => {
emailService.listEmails.mockResolvedValue({
emails: [],
total: 0,
Expand Down
9 changes: 5 additions & 4 deletions src/modules/email/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ export class EmailController {
@ApiOperation({
summary: 'List emails',
description:
'Paginated list of email summaries for a given mailbox. Defaults to the inbox.',
'Paginated list of email summaries. Filter by mailbox or omit to list all.',
})
@ApiQuery({
name: 'mailbox',
required: false,
enum: ['inbox', 'drafts', 'sent', 'trash', 'spam', 'archive'],
description: 'Mailbox to list. Defaults to `inbox`.',
description:
'Mailbox to filter by. Omit to list emails from all mailboxes.',
})
@ApiQuery({
name: 'limit',
Expand All @@ -90,14 +91,14 @@ export class EmailController {
@ApiOkResponse({ type: EmailListResponseDto })
list(
@User('email') email: string,
@Query('mailbox') mailbox: MailboxType = 'inbox',
@Query('mailbox') mailbox?: MailboxType,
@Query('limit') limit?: string,
@Query('position') position?: string,
@Query('anchorId') anchorId?: string,
) {
return this.emailService.listEmails(
email,
mailbox,
mailbox ?? undefined,
limit ? Number(limit) || 20 : 20,
position ? Number(position) || 0 : 0,
anchorId,
Expand Down
3 changes: 3 additions & 0 deletions src/modules/email/email.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export class EmailSummaryResponseDto {
@ApiProperty({ example: 'Ma1f09b…' })
id!: string;

@ApiProperty({ type: [String], example: ['d', 'a'] })
mailboxIds!: string[];

@ApiProperty({ example: 'T1a2b3c…' })
threadId!: string;

Expand Down
63 changes: 38 additions & 25 deletions src/modules/email/email.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach } from 'vitest';
import { Test } from '@nestjs/testing';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { createMock, type DeepMocked } from '@golevelup/ts-vitest';
import { EmailService } from './email.service.js';
import { type MailProvider } from './mail-provider.port.js';
import { MailProvider } from './mail-provider.port.js';
import {
newMailbox,
newEmail,
Expand All @@ -10,32 +12,20 @@ import {
newDraftEmailDto,
} from '../../../test/fixtures.js';

type MockMailProvider = {
[K in keyof MailProvider]: ReturnType<typeof vi.fn>;
};

function createMockMailProvider(): MockMailProvider {
return {
getMailboxes: vi.fn(),
listEmails: vi.fn(),
getEmail: vi.fn(),
sendEmail: vi.fn(),
saveDraft: vi.fn(),
moveEmail: vi.fn(),
deleteEmail: vi.fn(),
markAsRead: vi.fn(),
markAsFlagged: vi.fn(),
};
}

describe('EmailService', () => {
let service: EmailService;
let provider: MockMailProvider;
let provider: DeepMocked<MailProvider>;
const userEmail = 'test@example.com';

beforeEach(() => {
provider = createMockMailProvider();
service = new EmailService(provider);
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [EmailService],
})
.useMocker(() => createMock<MailProvider>())
.compile();

service = module.get(EmailService);
provider = module.get<DeepMocked<MailProvider>>(MailProvider);
});

describe('getMailboxes', () => {
Expand All @@ -51,10 +41,12 @@ describe('EmailService', () => {
});

describe('listEmails', () => {
it('when called, then delegates with all parameters', async () => {
it('when called with a mailbox, then delegates with mailbox', async () => {
const response = {
emails: [newEmailSummary()],
total: 1,
hasMoreMails: false,
nextAnchor: undefined,
};
provider.listEmails.mockResolvedValue(response);

Expand All @@ -69,6 +61,27 @@ describe('EmailService', () => {
);
expect(result).toBe(response);
});

it('when called without a mailbox, then delegates with undefined', async () => {
const response = {
emails: [newEmailSummary()],
total: 1,
hasMoreMails: false,
nextAnchor: undefined,
};
provider.listEmails.mockResolvedValue(response);

const result = await service.listEmails(userEmail, undefined, 20, 0);

expect(provider.listEmails).toHaveBeenCalledWith(
userEmail,
undefined,
20,
0,
undefined,
);
expect(result).toBe(response);
});
});

describe('getEmail', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/email/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class EmailService {

listEmails(
userEmail: string,
mailbox: MailboxType,
mailbox: MailboxType | undefined,
limit: number,
position: number,
anchorId?: string,
Expand Down
1 change: 1 addition & 0 deletions src/modules/email/email.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Mailbox {
export interface EmailSummary {
id: string;
threadId: string;
mailboxIds: string[];
from: EmailAddress[];
to: EmailAddress[];
subject: string;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/email/mail-provider.port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export abstract class MailProvider {
abstract getMailboxes(userEmail: string): Promise<Mailbox[]>;
abstract listEmails(
userEmail: string,
mailbox: MailboxType,
mailbox: MailboxType | undefined,
limit: number,
position: number,
anchorId?: string,
Expand Down
7 changes: 2 additions & 5 deletions src/modules/gateway/gateway.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { createMock, type DeepMocked } from '@golevelup/ts-vitest';
import { GatewayController } from './gateway.controller.js';
import { AccountService } from '../account/account.service.js';
import { DomainRepository } from '../account/repositories/domain.repository.js';
import { MailAccount } from '../account/domain/mail-account.domain.js';
import { MailDomain } from '../account/domain/mail-domain.domain.js';
import {
Expand All @@ -16,7 +15,6 @@ import { v4 } from 'uuid';
describe('GatewayController', () => {
let controller: GatewayController;
let accountService: DeepMocked<AccountService>;
let domains: DeepMocked<DomainRepository>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -27,7 +25,6 @@ describe('GatewayController', () => {

controller = module.get(GatewayController);
accountService = module.get(AccountService);
domains = module.get(DomainRepository);
});

describe('provisionAccount', () => {
Expand Down Expand Up @@ -74,7 +71,7 @@ describe('GatewayController', () => {
MailDomain.build(newMailDomainAttributes({ domain: 'internxt.com' })),
MailDomain.build(newMailDomainAttributes({ domain: 'internxt.me' })),
];
domains.findAllActive.mockResolvedValue(domainList);
accountService.listActiveDomains.mockResolvedValue(domainList);

const result = await controller.listDomains();

Expand All @@ -85,7 +82,7 @@ describe('GatewayController', () => {
});

it('when no active domains, then returns empty array', async () => {
domains.findAllActive.mockResolvedValue([]);
accountService.listActiveDomains.mockResolvedValue([]);

const result = await controller.listDomains();

Expand Down
8 changes: 2 additions & 6 deletions src/modules/gateway/gateway.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Public } from '../auth/decorators/public.decorator.js';
import { AccountService } from '../account/account.service.js';
import { DomainRepository } from '../account/repositories/domain.repository.js';
import { GatewayAuthGuard } from './gateway.guard.js';
import { ProvisionAccountRequestDto } from './gateway.dto.js';

Expand All @@ -24,10 +23,7 @@ import { ProvisionAccountRequestDto } from './gateway.dto.js';
export class GatewayController {
private readonly logger = new Logger(GatewayController.name);

constructor(
private readonly accountService: AccountService,
private readonly domains: DomainRepository,
) {}
constructor(private readonly accountService: AccountService) {}

@Post('accounts')
@HttpCode(HttpStatus.CREATED)
Expand Down Expand Up @@ -58,7 +54,7 @@ export class GatewayController {
summary: 'List available mail domains (called by the auth service)',
})
async listDomains() {
const activeDomains = await this.domains.findAllActive();
const activeDomains = await this.accountService.listActiveDomains();
return activeDomains.map((d) => ({ domain: d.domain }));
}

Expand Down
1 change: 1 addition & 0 deletions src/modules/infrastructure/jmap/jmap-mail.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function mapJmapEmailToSummary(e: JmapEmail): EmailSummary {
return {
id: e.id,
threadId: e.threadId,
mailboxIds: Object.keys(e.mailboxIds),
from: e.from ?? [],
to: e.to ?? [],
subject: e.subject ?? '',
Expand Down
Loading
Loading