@serve.zone/dcrouter 18.0.1 → 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1522 -1375
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +32 -4
- package/dist_ts/db/documents/classes.smtp-account.doc.d.ts +31 -0
- package/dist_ts/db/documents/classes.smtp-account.doc.js +176 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/email/classes.email-settings.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-settings.manager.js +30 -1
- package/dist_ts/email/classes.smtp-account.manager.d.ts +110 -0
- package/dist_ts/email/classes.smtp-account.manager.js +507 -0
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +23 -4
- package/dist_ts/email/classes.workapp-mail-manager.js +39 -46
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/smtp-scram.d.ts +13 -0
- package/dist_ts/email/smtp-scram.js +19 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/email-settings.handler.js +14 -2
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/smtp-account.handler.d.ts +20 -0
- package/dist_ts/opsserver/handlers/smtp-account.handler.js +155 -0
- package/dist_ts_interfaces/data/email-settings.d.ts +10 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +1 -1
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/data/smtp-account.d.ts +50 -0
- package/dist_ts_interfaces/data/smtp-account.js +9 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/smtp-accounts.d.ts +125 -0
- package/dist_ts_interfaces/requests/smtp-accounts.js +2 -0
- package/dist_ts_migrations/index.js +198 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/smtp-accounts.d.ts +38 -0
- package/dist_ts_web/appstate/smtp-accounts.js +94 -0
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/index.d.ts +1 -0
- package/dist_ts_web/elements/email/index.js +2 -1
- package/dist_ts_web/elements/email/ops-view-smtp-accounts.d.ts +25 -0
- package/dist_ts_web/elements/email/ops-view-smtp-accounts.js +598 -0
- package/dist_ts_web/elements/ops-dashboard.js +3 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/readme.md +12 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +35 -3
- package/ts/db/documents/classes.smtp-account.doc.ts +103 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/email/classes.email-settings.manager.ts +34 -0
- package/ts/email/classes.smtp-account.manager.ts +601 -0
- package/ts/email/classes.workapp-mail-manager.ts +53 -55
- package/ts/email/index.ts +2 -0
- package/ts/email/smtp-scram.ts +24 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/email-settings.handler.ts +13 -1
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/smtp-account.handler.ts +199 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/smtp-accounts.ts +143 -0
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/index.ts +1 -0
- package/ts_web/elements/email/ops-view-smtp-accounts.ts +586 -0
- package/ts_web/elements/ops-dashboard.ts +2 -0
- package/ts_web/router.ts +1 -1
|
@@ -93,6 +93,26 @@ interface IStoredWorkAppMailState {
|
|
|
93
93
|
identities: IStoredWorkAppMailIdentity[];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* WorkApp-generated routes and SMTP users contributed to the composed email
|
|
98
|
+
* runtime config. Consumed by SmtpAccountManager, the single owner of the
|
|
99
|
+
* final `auth` block and route-list composition.
|
|
100
|
+
*/
|
|
101
|
+
export interface IWorkAppMailRuntimeContribution {
|
|
102
|
+
routes: IEmailRoute[];
|
|
103
|
+
users: Array<{ username: string; password: string }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Route names generated (and owned wholesale) by WorkAppMailManager. */
|
|
107
|
+
export function isWorkAppManagedMailRouteName(routeName: string): boolean {
|
|
108
|
+
return routeName.startsWith('workapp-mail-');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** SMTP usernames generated (and owned wholesale) by WorkAppMailManager. */
|
|
112
|
+
export function isWorkAppManagedSmtpUsername(username: string): boolean {
|
|
113
|
+
return username.startsWith('workapp-');
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
export class WorkAppMailManager {
|
|
97
117
|
private readonly storageKey = '/workhosters/mail-identities.json';
|
|
98
118
|
private identityMutationPromise: Promise<unknown> = Promise.resolve();
|
|
@@ -659,12 +679,36 @@ export class WorkAppMailManager {
|
|
|
659
679
|
};
|
|
660
680
|
}
|
|
661
681
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Compute the workapp-generated routes and SMTP users for the given
|
|
684
|
+
* candidate email config. Pure contribution — the final composition of
|
|
685
|
+
* routes and the `auth` block is owned exclusively by SmtpAccountManager.
|
|
686
|
+
*/
|
|
687
|
+
public async getStoredIdentityContribution(
|
|
688
|
+
emailConfig: IUnifiedEmailServerOptions,
|
|
689
|
+
identities = undefined as IStoredWorkAppMailIdentity[] | undefined,
|
|
690
|
+
): Promise<IWorkAppMailRuntimeContribution> {
|
|
691
|
+
const nextIdentities = identities || await this.readStoredIdentities();
|
|
692
|
+
const readyDomains = await this.getReadyOutboundDomains(nextIdentities);
|
|
693
|
+
const generatedRoutes = nextIdentities
|
|
694
|
+
.filter((identity) => {
|
|
695
|
+
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
696
|
+
return identity.enabled && ['smtpForward', 'typedEndpoint'].includes(inboundTarget?.type || '');
|
|
697
|
+
})
|
|
698
|
+
.map((identity) => this.buildInboundRoute(identity));
|
|
699
|
+
const generatedOutboundRoutes = nextIdentities
|
|
700
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
701
|
+
.map((identity) => this.buildOutboundRoute(identity, emailConfig));
|
|
702
|
+
const generatedUsers = nextIdentities
|
|
703
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
704
|
+
.map((identity) => ({
|
|
705
|
+
username: identity.smtp.username,
|
|
706
|
+
password: identity.smtpPassword,
|
|
707
|
+
}));
|
|
708
|
+
return {
|
|
709
|
+
routes: [...generatedRoutes, ...generatedOutboundRoutes],
|
|
710
|
+
users: generatedUsers,
|
|
711
|
+
};
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
public async applyStoredIdentitiesToRuntime(
|
|
@@ -673,15 +717,8 @@ export class WorkAppMailManager {
|
|
|
673
717
|
const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
|
|
674
718
|
if (!emailConfig) return;
|
|
675
719
|
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
const nextConfig = this.mergeIdentitiesIntoEmailConfig(emailConfig, nextIdentities, readyDomains);
|
|
679
|
-
|
|
680
|
-
this.dcRouterRef.options.emailConfig = nextConfig;
|
|
681
|
-
if (this.dcRouterRef.emailServer) {
|
|
682
|
-
this.dcRouterRef.emailServer.updateOptions({ auth: nextConfig.auth });
|
|
683
|
-
await this.dcRouterRef.updateEmailRoutes(nextConfig.routes);
|
|
684
|
-
}
|
|
720
|
+
const contribution = await this.getStoredIdentityContribution(emailConfig, identities);
|
|
721
|
+
await this.dcRouterRef.smtpAccountManager.applyToRuntime(contribution);
|
|
685
722
|
}
|
|
686
723
|
|
|
687
724
|
private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
|
|
@@ -717,41 +754,6 @@ export class WorkAppMailManager {
|
|
|
717
754
|
>;
|
|
718
755
|
}
|
|
719
756
|
|
|
720
|
-
private mergeIdentitiesIntoEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
|
721
|
-
emailConfig: TConfig,
|
|
722
|
-
identities: IStoredWorkAppMailIdentity[],
|
|
723
|
-
readyDomains: Set<string>,
|
|
724
|
-
): TConfig {
|
|
725
|
-
const generatedRoutes = identities
|
|
726
|
-
.filter((identity) => {
|
|
727
|
-
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
728
|
-
return identity.enabled && ['smtpForward', 'typedEndpoint'].includes(inboundTarget?.type || '');
|
|
729
|
-
})
|
|
730
|
-
.map((identity) => this.buildInboundRoute(identity));
|
|
731
|
-
const generatedOutboundRoutes = identities
|
|
732
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
733
|
-
.map((identity) => this.buildOutboundRoute(identity, emailConfig));
|
|
734
|
-
const configuredRoutes = (emailConfig.routes || [])
|
|
735
|
-
.filter((route) => !this.isManagedMailRouteName(route.name));
|
|
736
|
-
const generatedUsers = identities
|
|
737
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
738
|
-
.map((identity) => ({
|
|
739
|
-
username: identity.smtp.username,
|
|
740
|
-
password: identity.smtpPassword,
|
|
741
|
-
}));
|
|
742
|
-
const configuredUsers = (emailConfig.auth?.users || [])
|
|
743
|
-
.filter((user) => !this.isManagedSmtpUsername(user.username));
|
|
744
|
-
|
|
745
|
-
return {
|
|
746
|
-
...emailConfig,
|
|
747
|
-
routes: [...configuredRoutes, ...generatedRoutes, ...generatedOutboundRoutes],
|
|
748
|
-
auth: {
|
|
749
|
-
...(emailConfig.auth || {}),
|
|
750
|
-
users: [...configuredUsers, ...generatedUsers],
|
|
751
|
-
},
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
757
|
private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
|
|
756
758
|
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
757
759
|
if (inboundTarget?.type === 'typedEndpoint') {
|
|
@@ -1594,12 +1596,8 @@ export class WorkAppMailManager {
|
|
|
1594
1596
|
return plugins.crypto.randomBytes(24).toString('base64url');
|
|
1595
1597
|
}
|
|
1596
1598
|
|
|
1597
|
-
private isManagedMailRouteName(routeName: string): boolean {
|
|
1598
|
-
return routeName.startsWith('workapp-mail-');
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
1599
|
private isManagedSmtpUsername(username: string): boolean {
|
|
1602
|
-
return username
|
|
1600
|
+
return isWorkAppManagedSmtpUsername(username);
|
|
1603
1601
|
}
|
|
1604
1602
|
|
|
1605
1603
|
private buildSmtpCredentials(
|
package/ts/email/index.ts
CHANGED
|
@@ -11,5 +11,7 @@ export * from './classes.email-settings.manager.js';
|
|
|
11
11
|
export * from './classes.smartmta-blob-storage-manager.js';
|
|
12
12
|
export * from './classes.smartmta-storage-manager.js';
|
|
13
13
|
export * from './smartmta-storage-policy.js';
|
|
14
|
+
export * from './classes.smtp-account.manager.js';
|
|
15
|
+
export * from './smtp-scram.js';
|
|
14
16
|
export * from './classes.workapp-mail-manager.js';
|
|
15
17
|
export * from './email-dns-records.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { deriveScramVerifier } from '@push.rocks/smartmta';
|
|
3
|
+
import type { ISmtpScramVerifier } from '@push.rocks/smartmta';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive an RFC 5802 SCRAM-SHA-256 verifier from a plaintext password using
|
|
7
|
+
* smartmta's canonical exported derivation (fresh random salt, OWASP-aligned
|
|
8
|
+
* default iteration count) — the exact algorithm the server verifies at AUTH
|
|
9
|
+
* time. Only the verifier is ever persisted; the plaintext is shown once at
|
|
10
|
+
* create/rotate time and then discarded.
|
|
11
|
+
*/
|
|
12
|
+
export function deriveSmtpScramVerifier(
|
|
13
|
+
password: string,
|
|
14
|
+
iterations?: number,
|
|
15
|
+
): ISmtpScramVerifier {
|
|
16
|
+
return deriveScramVerifier(password, iterations);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Machine-generated SMTP account password (~192 bits of entropy). Shown once.
|
|
21
|
+
*/
|
|
22
|
+
export function generateSmtpAccountPassword(): string {
|
|
23
|
+
return plugins.crypto.randomBytes(24).toString('base64url');
|
|
24
|
+
}
|
|
@@ -41,6 +41,7 @@ export class OpsServer {
|
|
|
41
41
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
|
42
42
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
43
43
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
44
|
+
private smtpAccountHandler!: handlers.SmtpAccountHandler;
|
|
44
45
|
private gatewayClientHandler!: handlers.GatewayClientHandler;
|
|
45
46
|
private webPushControlHandler!: handlers.WebPushControlHandler;
|
|
46
47
|
private webPushAppHandler!: handlers.WebPushAppHandler;
|
|
@@ -109,6 +110,7 @@ export class OpsServer {
|
|
|
109
110
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
|
110
111
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
111
112
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
113
|
+
this.smtpAccountHandler = new handlers.SmtpAccountHandler(this);
|
|
112
114
|
this.gatewayClientHandler = new handlers.GatewayClientHandler(this);
|
|
113
115
|
this.webPushControlHandler = new handlers.WebPushControlHandler(this);
|
|
114
116
|
this.webPushAppHandler = new handlers.WebPushAppHandler(this);
|
|
@@ -54,9 +54,19 @@ export class EmailSettingsHandler {
|
|
|
54
54
|
const mailerBridgeState = dcRouter.emailServer?.getBridgeState() as
|
|
55
55
|
| interfaces.data.TEmailBridgeState
|
|
56
56
|
| undefined;
|
|
57
|
+
const smtpAccountCount = dcRouter.smtpAccountManager?.getAccountCount() ?? 0;
|
|
58
|
+
const smtpListenerActive = dcRouter.emailServer
|
|
59
|
+
? dcRouter.emailServer.isSmtpListenerActive()
|
|
60
|
+
: undefined;
|
|
57
61
|
const manager = dcRouter.emailSettingsManager;
|
|
58
62
|
if (manager) {
|
|
59
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
...manager.getPublicSettings(),
|
|
65
|
+
outboundEgress,
|
|
66
|
+
mailerBridgeState,
|
|
67
|
+
smtpAccountCount,
|
|
68
|
+
...(smtpListenerActive !== undefined ? { smtpListenerActive } : {}),
|
|
69
|
+
};
|
|
60
70
|
}
|
|
61
71
|
const emailConfig = dcRouter.options.emailConfig;
|
|
62
72
|
const emailPortConfig = dcRouter.options.emailPortConfig;
|
|
@@ -75,6 +85,8 @@ export class EmailSettingsHandler {
|
|
|
75
85
|
domainCount: emailConfig?.domains?.length || 0,
|
|
76
86
|
routeCount: emailConfig?.routes?.length || 0,
|
|
77
87
|
authUserCount: emailConfig?.auth?.users?.length || 0,
|
|
88
|
+
smtpAccountCount,
|
|
89
|
+
...(smtpListenerActive !== undefined ? { smtpListenerActive } : {}),
|
|
78
90
|
updatedAt: 0,
|
|
79
91
|
updatedBy: 'runtime-options',
|
|
80
92
|
};
|
|
@@ -21,6 +21,7 @@ export * from './dns-authority.handler.js';
|
|
|
21
21
|
export * from './dns-record.handler.js';
|
|
22
22
|
export * from './acme-config.handler.js';
|
|
23
23
|
export * from './email-domain.handler.js';
|
|
24
|
+
export * from './smtp-account.handler.js';
|
|
24
25
|
export * from './gatewayclient.handler.js';
|
|
25
26
|
export * from './realtime.handler.js';
|
|
26
27
|
export * from './config-event.handler.js';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
|
+
import { logger } from '../../logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CRUD handler for operator-managed SMTP submission accounts, plus the
|
|
9
|
+
* legacy-route retirement lever.
|
|
10
|
+
*
|
|
11
|
+
* Auth: admin JWT or API token with `smtp-accounts:read` / `smtp-accounts:write`
|
|
12
|
+
* scope; every write additionally requires an admin identity when a JWT is
|
|
13
|
+
* used. Successful admin actions are audit-logged to the ops log.
|
|
14
|
+
*/
|
|
15
|
+
export class SmtpAccountHandler {
|
|
16
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
17
|
+
|
|
18
|
+
constructor(private opsServerRef: OpsServer) {
|
|
19
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
20
|
+
this.registerHandlers();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async requireAuth(
|
|
24
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
25
|
+
requiredScope: interfaces.data.TApiTokenScope,
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
28
|
+
scope: requiredScope,
|
|
29
|
+
requireAdminIdentity: requiredScope.endsWith(':write'),
|
|
30
|
+
});
|
|
31
|
+
return auth.userId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private get manager() {
|
|
35
|
+
return this.opsServerRef.dcRouterRef.smtpAccountManager;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private failure(errorArg: unknown, fallbackMessageArg: string): interfaces.requests.ISmtpAccountActionResult {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg,
|
|
42
|
+
code: 'SMTP_ACCOUNT_ACTION_FAILED',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private registerHandlers(): void {
|
|
47
|
+
this.typedrouter.addTypedHandler(
|
|
48
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSmtpAccounts>(
|
|
49
|
+
'getSmtpAccounts',
|
|
50
|
+
async (dataArg) => {
|
|
51
|
+
await this.requireAuth(dataArg, 'smtp-accounts:read');
|
|
52
|
+
if (!this.manager?.isStarted()) return { accounts: [] };
|
|
53
|
+
return { accounts: await this.manager.listAccounts() };
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this.typedrouter.addTypedHandler(
|
|
59
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSmtpAccount>(
|
|
60
|
+
'createSmtpAccount',
|
|
61
|
+
async (dataArg) => {
|
|
62
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
63
|
+
if (!this.manager?.isStarted()) {
|
|
64
|
+
return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const result = await this.manager.createAccount({
|
|
68
|
+
username: dataArg.username,
|
|
69
|
+
description: dataArg.description,
|
|
70
|
+
senderScope: dataArg.senderScope,
|
|
71
|
+
recipientScope: dataArg.recipientScope,
|
|
72
|
+
mailPolicy: dataArg.mailPolicy,
|
|
73
|
+
createdBy: userId,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
|
78
|
+
account: result.account,
|
|
79
|
+
password: result.password,
|
|
80
|
+
};
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
return this.failure(err, 'Failed to create SMTP account');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
this.typedrouter.addTypedHandler(
|
|
89
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSmtpAccount>(
|
|
90
|
+
'updateSmtpAccount',
|
|
91
|
+
async (dataArg) => {
|
|
92
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
93
|
+
if (!this.manager?.isStarted()) {
|
|
94
|
+
return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.manager.updateAccount(dataArg.id, {
|
|
98
|
+
description: dataArg.description,
|
|
99
|
+
senderScope: dataArg.senderScope,
|
|
100
|
+
recipientScope: dataArg.recipientScope,
|
|
101
|
+
mailPolicy: dataArg.mailPolicy,
|
|
102
|
+
}, userId);
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
|
106
|
+
account: result.account,
|
|
107
|
+
};
|
|
108
|
+
} catch (err: unknown) {
|
|
109
|
+
return this.failure(err, 'Failed to update SMTP account');
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.typedrouter.addTypedHandler(
|
|
116
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleSmtpAccount>(
|
|
117
|
+
'toggleSmtpAccount',
|
|
118
|
+
async (dataArg) => {
|
|
119
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
120
|
+
if (!this.manager?.isStarted()) {
|
|
121
|
+
return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = await this.manager.toggleAccount(dataArg.id, dataArg.enabled, userId);
|
|
125
|
+
return { success: true, account: result.account };
|
|
126
|
+
} catch (err: unknown) {
|
|
127
|
+
return this.failure(err, 'Failed to toggle SMTP account');
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
this.typedrouter.addTypedHandler(
|
|
134
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RotateSmtpAccountPassword>(
|
|
135
|
+
'rotateSmtpAccountPassword',
|
|
136
|
+
async (dataArg) => {
|
|
137
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
138
|
+
if (!this.manager?.isStarted()) {
|
|
139
|
+
return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const result = await this.manager.rotatePassword(dataArg.id, userId);
|
|
143
|
+
return { success: true, account: result.account, password: result.password };
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
return this.failure(err, 'Failed to rotate SMTP account password');
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
this.typedrouter.addTypedHandler(
|
|
152
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSmtpAccount>(
|
|
153
|
+
'deleteSmtpAccount',
|
|
154
|
+
async (dataArg) => {
|
|
155
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
156
|
+
if (!this.manager?.isStarted()) {
|
|
157
|
+
return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await this.manager.deleteAccount(dataArg.id, userId);
|
|
161
|
+
return { success: true };
|
|
162
|
+
} catch (err: unknown) {
|
|
163
|
+
return this.failure(err, 'Failed to delete SMTP account');
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Internal-relay retirement lever: removes one persisted legacy route by
|
|
170
|
+
// exact name through the standard settings restart path. Guarded in
|
|
171
|
+
// EmailSettingsManager (unknown names and generated managed names fail).
|
|
172
|
+
this.typedrouter.addTypedHandler(
|
|
173
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveLegacyEmailRoute>(
|
|
174
|
+
'removeLegacyEmailRoute',
|
|
175
|
+
async (dataArg) => {
|
|
176
|
+
const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
|
|
177
|
+
const routeName = (dataArg.routeName || '').trim();
|
|
178
|
+
logger.log('info', `removeLegacyEmailRoute invoked by ${userId} for route '${routeName}'`);
|
|
179
|
+
if (!routeName) {
|
|
180
|
+
return { success: false, message: 'routeName is required', code: 'SMTP_ACCOUNT_ACTION_FAILED' };
|
|
181
|
+
}
|
|
182
|
+
if (!this.opsServerRef.dcRouterRef.emailSettingsManager) {
|
|
183
|
+
return { success: false, message: 'EmailSettingsManager not initialized', code: 'EMAIL_SETTINGS_MANAGER_UNAVAILABLE' };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await this.opsServerRef.dcRouterRef.updateEmailServerSettings(
|
|
187
|
+
{ removeRouteNames: [routeName] },
|
|
188
|
+
userId,
|
|
189
|
+
);
|
|
190
|
+
logger.log('info', `Legacy email route '${routeName}' removed from persisted settings by ${userId}`);
|
|
191
|
+
return { success: true, message: `Route '${routeName}' removed` };
|
|
192
|
+
} catch (err: unknown) {
|
|
193
|
+
return this.failure(err, 'Failed to remove legacy email route');
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { appState } from './shared.js';
|
|
4
|
+
import { getActionContext } from './login.js';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// SMTP Accounts State
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface ISmtpAccountsState {
|
|
11
|
+
accounts: interfaces.data.ISmtpAccountInfo[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
lastUpdated: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const smtpAccountsStatePart = await appState.getStatePart<ISmtpAccountsState>(
|
|
18
|
+
'smtpAccounts',
|
|
19
|
+
{
|
|
20
|
+
accounts: [],
|
|
21
|
+
isLoading: false,
|
|
22
|
+
error: null,
|
|
23
|
+
lastUpdated: 0,
|
|
24
|
+
},
|
|
25
|
+
'soft',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const fetchSmtpAccountsAction = smtpAccountsStatePart.createAction(
|
|
29
|
+
async (statePartArg): Promise<ISmtpAccountsState> => {
|
|
30
|
+
const context = getActionContext();
|
|
31
|
+
const currentState = statePartArg.getState()!;
|
|
32
|
+
if (!context.identity) return currentState;
|
|
33
|
+
try {
|
|
34
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
35
|
+
interfaces.requests.IReq_GetSmtpAccounts
|
|
36
|
+
>('/typedrequest', 'getSmtpAccounts');
|
|
37
|
+
const response = await request.fire({ identity: context.identity });
|
|
38
|
+
return {
|
|
39
|
+
...statePartArg.getState()!,
|
|
40
|
+
accounts: response.accounts,
|
|
41
|
+
isLoading: false,
|
|
42
|
+
error: null,
|
|
43
|
+
lastUpdated: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
} catch (error: unknown) {
|
|
46
|
+
return {
|
|
47
|
+
...statePartArg.getState()!,
|
|
48
|
+
isLoading: false,
|
|
49
|
+
error: error instanceof Error ? error.message : 'Failed to fetch SMTP accounts',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const toggleSmtpAccountAction = smtpAccountsStatePart.createAction<{
|
|
56
|
+
id: string;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<ISmtpAccountsState> => {
|
|
59
|
+
const context = getActionContext();
|
|
60
|
+
const currentState = statePartArg.getState()!;
|
|
61
|
+
try {
|
|
62
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
63
|
+
interfaces.requests.IReq_ToggleSmtpAccount
|
|
64
|
+
>('/typedrequest', 'toggleSmtpAccount');
|
|
65
|
+
const response = await request.fire({
|
|
66
|
+
identity: context.identity!,
|
|
67
|
+
id: dataArg.id,
|
|
68
|
+
enabled: dataArg.enabled,
|
|
69
|
+
});
|
|
70
|
+
if (!response.success) {
|
|
71
|
+
return { ...currentState, error: response.message || 'Failed to toggle SMTP account' };
|
|
72
|
+
}
|
|
73
|
+
return await actionContext!.dispatch(fetchSmtpAccountsAction, null);
|
|
74
|
+
} catch (error: unknown) {
|
|
75
|
+
return {
|
|
76
|
+
...currentState,
|
|
77
|
+
error: error instanceof Error ? error.message : 'Failed to toggle SMTP account',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const deleteSmtpAccountAction = smtpAccountsStatePart.createAction<string>(
|
|
83
|
+
async (statePartArg, id, actionContext): Promise<ISmtpAccountsState> => {
|
|
84
|
+
const context = getActionContext();
|
|
85
|
+
const currentState = statePartArg.getState()!;
|
|
86
|
+
try {
|
|
87
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
88
|
+
interfaces.requests.IReq_DeleteSmtpAccount
|
|
89
|
+
>('/typedrequest', 'deleteSmtpAccount');
|
|
90
|
+
const response = await request.fire({ identity: context.identity!, id });
|
|
91
|
+
if (!response.success) {
|
|
92
|
+
return { ...currentState, error: response.message || 'Failed to delete SMTP account' };
|
|
93
|
+
}
|
|
94
|
+
return await actionContext!.dispatch(fetchSmtpAccountsAction, null);
|
|
95
|
+
} catch (error: unknown) {
|
|
96
|
+
return {
|
|
97
|
+
...currentState,
|
|
98
|
+
error: error instanceof Error ? error.message : 'Failed to delete SMTP account',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Secret-returning calls — standalone functions, deliberately NOT actions:
|
|
106
|
+
// the one-time password must never enter (persisted) app state.
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
export async function createSmtpAccount(options: {
|
|
110
|
+
username: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
senderScope?: interfaces.data.ISmtpAccountSenderScope;
|
|
113
|
+
recipientScope?: interfaces.data.ISmtpAccountRecipientScope;
|
|
114
|
+
mailPolicy?: interfaces.data.ISmtpAccountMailPolicy;
|
|
115
|
+
}) {
|
|
116
|
+
const context = getActionContext();
|
|
117
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
118
|
+
interfaces.requests.IReq_CreateSmtpAccount
|
|
119
|
+
>('/typedrequest', 'createSmtpAccount');
|
|
120
|
+
return request.fire({ identity: context.identity!, ...options });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function rotateSmtpAccountPassword(id: string) {
|
|
124
|
+
const context = getActionContext();
|
|
125
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
126
|
+
interfaces.requests.IReq_RotateSmtpAccountPassword
|
|
127
|
+
>('/typedrequest', 'rotateSmtpAccountPassword');
|
|
128
|
+
return request.fire({ identity: context.identity!, id });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function updateSmtpAccount(options: {
|
|
132
|
+
id: string;
|
|
133
|
+
description?: string;
|
|
134
|
+
senderScope?: interfaces.data.ISmtpAccountSenderScope;
|
|
135
|
+
recipientScope?: interfaces.data.ISmtpAccountRecipientScope;
|
|
136
|
+
mailPolicy?: interfaces.data.ISmtpAccountMailPolicy;
|
|
137
|
+
}) {
|
|
138
|
+
const context = getActionContext();
|
|
139
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
140
|
+
interfaces.requests.IReq_UpdateSmtpAccount
|
|
141
|
+
>('/typedrequest', 'updateSmtpAccount');
|
|
142
|
+
return request.fire({ identity: context.identity!, ...options });
|
|
143
|
+
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from './appstate/domains.js';
|
|
|
20
20
|
export * from './appstate/routes.js';
|
|
21
21
|
export * from './appstate/users.js';
|
|
22
22
|
export * from './appstate/email-domains.js';
|
|
23
|
+
export * from './appstate/smtp-accounts.js';
|
|
23
24
|
export * from './appstate/realtime.js';
|
|
24
25
|
|
|
25
26
|
// Side-effect module: auto-refresh scheduling and TypedSocket log streaming.
|