Skip to content

Commit 1f0ee38

Browse files
committed
refactor: update StalwartAccountProvider and StalwartService for account management
- Migrated to use stalwarts jmap management api - Enhanced createAccount method to resolve domain and create accounts with proper error handling. - Updated deleteAccount method to use email for account deletion. - Refactored getAccount method to retrieve account information by email, returning structured data. - Adjusted tests to reflect changes in account creation and retrieval logic, ensuring proper error handling for domain resolution and account existence.
1 parent b90da4c commit 1f0ee38

4 files changed

Lines changed: 554 additions & 265 deletions

File tree

src/modules/infrastructure/stalwart/stalwart-account.provider.spec.ts

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
22
import { Test, type TestingModule } from '@nestjs/testing';
33
import { createMock, type DeepMocked } from '@golevelup/ts-vitest';
44
import { StalwartAccountProvider } from './stalwart-account.provider.js';
5-
import { StalwartService } from './stalwart.service.js';
5+
import { StalwartApiError, StalwartService } from './stalwart.service.js';
66
import { newCreateAccountParams } from '../../../../test/fixtures.js';
77

88
describe('StalwartAccountProvider', () => {
@@ -21,29 +21,47 @@ describe('StalwartAccountProvider', () => {
2121
});
2222

2323
describe('createAccount', () => {
24-
it('when called, then creates principal with correct shape', async () => {
25-
const params = newCreateAccountParams();
24+
it('when called, then resolves the domain and creates the account', async () => {
25+
const params = newCreateAccountParams({
26+
primaryAddress: 'alice@example.com',
27+
});
28+
stalwart.resolveDomainId.mockResolvedValue('dom1');
2629

2730
await provider.createAccount(params);
2831

29-
expect(stalwart.createPrincipal).toHaveBeenCalledWith({
30-
type: 'individual',
31-
name: params.primaryAddress,
32+
expect(stalwart.resolveDomainId).toHaveBeenCalledWith('example.com');
33+
expect(stalwart.createAccount).toHaveBeenCalledWith({
34+
name: 'alice',
35+
domainId: 'dom1',
3236
description: params.displayName,
33-
secrets: [params.password],
34-
emails: [params.primaryAddress],
35-
quota: 0,
36-
roles: ['user'],
37+
password: params.password,
38+
quotaBytes: params.quota ?? 0,
39+
});
40+
});
41+
42+
it('when domain is not configured, then throws and does not create', async () => {
43+
const params = newCreateAccountParams({
44+
primaryAddress: 'alice@unknown.com',
3745
});
46+
stalwart.resolveDomainId.mockResolvedValue(null);
47+
48+
await expect(provider.createAccount(params)).rejects.toThrow(
49+
StalwartApiError,
50+
);
51+
expect(stalwart.createAccount).not.toHaveBeenCalled();
3852
});
3953

4054
it('when quota is undefined, then defaults to 0', async () => {
41-
const params = newCreateAccountParams({ quota: undefined });
55+
const params = newCreateAccountParams({
56+
primaryAddress: 'alice@example.com',
57+
quota: undefined,
58+
});
59+
stalwart.resolveDomainId.mockResolvedValue('dom1');
4260

4361
await provider.createAccount(params);
4462

45-
expect(stalwart.createPrincipal).toHaveBeenCalledWith(
46-
expect.objectContaining({ quota: 0 }),
63+
expect(stalwart.createAccount).toHaveBeenCalledWith(
64+
expect.objectContaining({ quotaBytes: 0 }),
4765
);
4866
});
4967
});
@@ -52,50 +70,57 @@ describe('StalwartAccountProvider', () => {
5270
it('when called, then delegates to stalwart service', async () => {
5371
await provider.deleteAccount('user@example.com');
5472

55-
expect(stalwart.deletePrincipal).toHaveBeenCalledWith('user@example.com');
73+
expect(stalwart.deleteAccountByEmail).toHaveBeenCalledWith(
74+
'user@example.com',
75+
);
5676
});
5777
});
5878

5979
describe('getAccount', () => {
60-
it('when principal exists, then returns account info', async () => {
61-
stalwart.getPrincipal.mockResolvedValue({
62-
name: 'user@example.com',
63-
type: 'individual',
80+
it('when account exists, then returns AccountInfo with full email as name', async () => {
81+
stalwart.getAccountByEmail.mockResolvedValue({
82+
id: 'acc1',
83+
'@type': 'User',
84+
name: 'user',
85+
emailAddress: 'user@example.com',
86+
domainId: 'dom1',
6487
description: 'User Name',
65-
emails: ['user@example.com', 'alias@example.com'],
66-
quota: 5_000_000,
88+
quotas: { maxDiskQuota: 5_000_000 },
6789
});
6890

6991
const result = await provider.getAccount('user@example.com');
7092

7193
expect(result).toEqual({
7294
name: 'user@example.com',
7395
displayName: 'User Name',
74-
emails: ['user@example.com', 'alias@example.com'],
96+
emails: ['user@example.com'],
7597
quota: 5_000_000,
7698
});
7799
});
78100

79-
it('when principal does not exist, then returns null', async () => {
80-
stalwart.getPrincipal.mockResolvedValue(null);
101+
it('when account does not exist, then returns null', async () => {
102+
stalwart.getAccountByEmail.mockResolvedValue(null);
81103

82104
const result = await provider.getAccount('nonexistent@example.com');
83105

84106
expect(result).toBeNull();
85107
});
86108

87-
it('when principal has no optional fields, then uses defaults', async () => {
88-
stalwart.getPrincipal.mockResolvedValue({
89-
name: 'user@example.com',
90-
type: 'individual',
109+
it('when account has no optional fields, then uses defaults', async () => {
110+
stalwart.getAccountByEmail.mockResolvedValue({
111+
id: 'acc1',
112+
'@type': 'User',
113+
name: 'user',
114+
emailAddress: 'user@example.com',
115+
domainId: 'dom1',
91116
});
92117

93118
const result = await provider.getAccount('user@example.com');
94119

95120
expect(result).toEqual({
96121
name: 'user@example.com',
97122
displayName: '',
98-
emails: [],
123+
emails: ['user@example.com'],
99124
quota: 0,
100125
});
101126
});

src/modules/infrastructure/stalwart/stalwart-account.provider.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type {
44
AccountInfo,
55
CreateAccountParams,
66
} from '../../account/account.types.js';
7-
import { StalwartService } from './stalwart.service.js';
7+
import {
8+
StalwartApiError,
9+
StalwartService,
10+
splitEmail,
11+
} from './stalwart.service.js';
812

913
@Injectable()
1014
export class StalwartAccountProvider extends AccountProvider {
@@ -15,33 +19,40 @@ export class StalwartAccountProvider extends AccountProvider {
1519
}
1620

1721
async createAccount(params: CreateAccountParams): Promise<void> {
18-
await this.stalwart.createPrincipal({
19-
type: 'individual',
20-
name: params.primaryAddress,
22+
const { local, domain } = splitEmail(params.primaryAddress);
23+
const domainId = await this.stalwart.resolveDomainId(domain);
24+
if (!domainId) {
25+
throw new StalwartApiError(
26+
`Cannot create account: domain '${domain}' is not configured in Stalwart`,
27+
{ domain },
28+
);
29+
}
30+
31+
await this.stalwart.createAccount({
32+
name: local,
33+
domainId,
2134
description: params.displayName,
22-
secrets: [params.password],
23-
emails: [params.primaryAddress],
24-
quota: params.quota ?? 0,
25-
roles: ['user'],
35+
password: params.password,
36+
quotaBytes: params.quota ?? 0,
2637
});
2738

2839
this.logger.log(`Created account '${params.primaryAddress}'`);
2940
}
3041

31-
async deleteAccount(name: string): Promise<void> {
32-
await this.stalwart.deletePrincipal(name);
33-
this.logger.log(`Deleted account '${name}'`);
42+
async deleteAccount(email: string): Promise<void> {
43+
await this.stalwart.deleteAccountByEmail(email);
44+
this.logger.log(`Deleted account '${email}'`);
3445
}
3546

36-
async getAccount(name: string): Promise<AccountInfo | null> {
37-
const principal = await this.stalwart.getPrincipal(name);
38-
if (!principal) return null;
47+
async getAccount(email: string): Promise<AccountInfo | null> {
48+
const account = await this.stalwart.getAccountByEmail(email);
49+
if (!account) return null;
3950

4051
return {
41-
name: principal.name,
42-
displayName: principal.description ?? '',
43-
emails: principal.emails ?? [],
44-
quota: principal.quota ?? 0,
52+
name: account.emailAddress,
53+
displayName: account.description ?? '',
54+
emails: [account.emailAddress],
55+
quota: account.quotas?.maxDiskQuota ?? 0,
4556
};
4657
}
4758
}

0 commit comments

Comments
 (0)