mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
fix(email): test saved SMTP config and add TLS modes (#3388)
This commit is contained in:
1 parent
45b78a13c2
commit
cca8227ca6
66 files changed
+2464
-319
No files matched your search
@@ -13,6 +13,7 @@ export const ACCOUNT_IDENTITY_MODE_NAMES = ['email', 'username'] as const;
|
||||
type AccountIdentityModeName = (typeof ACCOUNT_IDENTITY_MODE_NAMES)[number];
|
||||
export const TAG_STYLE_NAMES = ['none', 'random'] as const;
|
||||
type TagStyleName = (typeof TAG_STYLE_NAMES)[number];
|
||||
type SmtpTlsModeName = 'implicit' | 'starttls' | 'opportunistic' | 'none';
|
||||
|
||||
interface InstanceBrandingConfig {
|
||||
product_name: string;
|
||||
@@ -189,7 +190,8 @@ export interface MasterConfig {
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
secure: boolean;
|
||||
tls_mode?: SmtpTlsModeName;
|
||||
secure?: boolean;
|
||||
};
|
||||
};
|
||||
voice: {
|
||||
|
||||
@@ -474,6 +474,25 @@ describe('ConfigLoader', () => {
|
||||
expect(config.instance.tag_style).toBe(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['implicit', 'implicit'],
|
||||
[' StartTLS ', 'starttls'],
|
||||
['opportunistic', 'opportunistic'],
|
||||
['none', 'none'],
|
||||
['', undefined],
|
||||
])('parses FLUXER_EMAIL_SMTP_TLS_MODE=%j', async (raw, expected) => {
|
||||
stubMinimalEnv({FLUXER_EMAIL_SMTP_HOST: 'smtp.example.com', FLUXER_EMAIL_SMTP_TLS_MODE: raw});
|
||||
const config = await loadConfig();
|
||||
expect(config.integrations.email.smtp?.tls_mode).toBe(expected);
|
||||
});
|
||||
|
||||
test.each(['tls', 'true'])('rejects the SMTP TLS mode %j', async (raw) => {
|
||||
stubMinimalEnv({FLUXER_EMAIL_SMTP_TLS_MODE: raw});
|
||||
await expect(loadConfig()).rejects.toThrow(
|
||||
'FLUXER_EMAIL_SMTP_TLS_MODE must be implicit, starttls, opportunistic or none',
|
||||
);
|
||||
});
|
||||
|
||||
test.each(['sequential', 'zero_first'])('rejects the tag style %j', async (raw) => {
|
||||
stubMinimalEnv({FLUXER_TAG_STYLE: raw});
|
||||
await expect(loadConfig()).rejects.toThrow('FLUXER_TAG_STYLE must be none or random');
|
||||
|
||||
@@ -163,6 +163,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
|
||||
FLUXER_EMAIL_SMTP_PORT: {path: ['integrations', 'email', 'smtp', 'port'], parse: parseInteger},
|
||||
FLUXER_EMAIL_SMTP_USERNAME: {path: ['integrations', 'email', 'smtp', 'username']},
|
||||
FLUXER_EMAIL_SMTP_PASSWORD: {path: ['integrations', 'email', 'smtp', 'password']},
|
||||
FLUXER_EMAIL_SMTP_TLS_MODE: {path: ['integrations', 'email', 'smtp', 'tls_mode'], parse: parseSmtpTlsMode},
|
||||
FLUXER_EMAIL_SMTP_SECURE: {path: ['integrations', 'email', 'smtp', 'secure'], parse: parseBoolean},
|
||||
FLUXER_LIVEKIT_ENABLED: {path: ['integrations', 'voice', 'enabled'], parse: parseBoolean},
|
||||
FLUXER_LIVEKIT_API_KEY: {path: ['integrations', 'voice', 'api_key']},
|
||||
@@ -350,6 +351,12 @@ function parseTagStyle(raw: string): string {
|
||||
throw new Error('must be none or random');
|
||||
}
|
||||
|
||||
function parseSmtpTlsMode(raw: string): string {
|
||||
const value = raw.trim().toLowerCase();
|
||||
if (value === 'implicit' || value === 'starttls' || value === 'opportunistic' || value === 'none') return value;
|
||||
throw new Error('must be implicit, starttls, opportunistic or none');
|
||||
}
|
||||
|
||||
function parseJson(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
|
||||
export const SmtpTlsModes = {
|
||||
IMPLICIT: 'implicit',
|
||||
STARTTLS: 'starttls',
|
||||
OPPORTUNISTIC: 'opportunistic',
|
||||
NONE: 'none',
|
||||
} as const;
|
||||
|
||||
export type SmtpTlsMode = ValueOf<typeof SmtpTlsModes>;
|
||||
|
||||
const SMTP_IMPLICIT_TLS_PORT = 465;
|
||||
|
||||
export function resolveSmtpTlsMode(mode: SmtpTlsMode | null | undefined, port: number): SmtpTlsMode {
|
||||
if (mode) return mode;
|
||||
return port === SMTP_IMPLICIT_TLS_PORT ? SmtpTlsModes.IMPLICIT : SmtpTlsModes.STARTTLS;
|
||||
}
|
||||
|
||||
export function smtpTlsModeFromSecure(secure: boolean | null | undefined): SmtpTlsMode | null {
|
||||
if (secure == null) return null;
|
||||
return secure ? SmtpTlsModes.IMPLICIT : SmtpTlsModes.OPPORTUNISTIC;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SystemChannelFlagsDescriptions,
|
||||
} from '@fluxer/constants/src/GuildConstants';
|
||||
import {LIMIT_KEY_METADATA, LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {SmtpTlsModes} from '@fluxer/constants/src/SmtpConstants';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/AdminAclType';
|
||||
import {AdminArchiveResponseSchema} from '@fluxer/schema/src/domains/admin/AdminArchiveSchemas';
|
||||
@@ -627,6 +628,16 @@ const InstancePolicyResponse = z.object({
|
||||
|
||||
const EmailProviderSchema = z.enum(['smtp', 'none']);
|
||||
|
||||
const SmtpTlsModeSchema = createNamedStringLiteralUnion(
|
||||
[
|
||||
[SmtpTlsModes.IMPLICIT, 'IMPLICIT', 'TLS from the first byte, usually on port 465'],
|
||||
[SmtpTlsModes.STARTTLS, 'STARTTLS', 'Plain connection that must upgrade with STARTTLS, usually on port 587'],
|
||||
[SmtpTlsModes.OPPORTUNISTIC, 'OPPORTUNISTIC', 'Plain connection upgraded with STARTTLS when the server offers it'],
|
||||
[SmtpTlsModes.NONE, 'NONE', 'No TLS, even when the server offers STARTTLS'],
|
||||
],
|
||||
'How the SMTP connection is encrypted',
|
||||
);
|
||||
|
||||
const AttachmentDecayEffectiveResponse = z.object({
|
||||
enabled: z.boolean(),
|
||||
min_size_mb: z.number().positive(),
|
||||
@@ -675,7 +686,8 @@ const InstanceIntegrationsResponse = z.object({
|
||||
port: z.number().int().min(1).max(65535).nullable(),
|
||||
username: z.string().nullable(),
|
||||
password_set: z.boolean(),
|
||||
secure: z.boolean().nullable(),
|
||||
tls_mode: SmtpTlsModeSchema.nullable().describe('Saved TLS mode, or null to pick one from the port'),
|
||||
effective_tls_mode: SmtpTlsModeSchema.nullable(),
|
||||
}),
|
||||
disable_new_ip_authorization: z.boolean(),
|
||||
effective_disable_new_ip_authorization: z.boolean(),
|
||||
@@ -789,8 +801,10 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
port: z.number().int().min(1).max(65535).nullish(),
|
||||
username: z.string().trim().max(320).nullish(),
|
||||
password: z.string().trim().max(4096).nullish(),
|
||||
secure: z.boolean().nullish(),
|
||||
tls_mode: SmtpTlsModeSchema.nullish(),
|
||||
secure: z.boolean().nullish().describe('Deprecated, use tls_mode'),
|
||||
})
|
||||
.register(schemaMetadata, {preserveNullFields: true})
|
||||
.nullish(),
|
||||
disable_new_ip_authorization: z.boolean().nullish(),
|
||||
})
|
||||
@@ -847,11 +861,11 @@ export const BrandingAssetUploadRequest = z.object({
|
||||
export type BrandingAssetUploadRequest = z.infer<typeof BrandingAssetUploadRequest>;
|
||||
|
||||
export const InstanceEmailSmtpTestRequest = z.object({
|
||||
host: z.string().trim().min(1).max(255),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
username: z.string().trim().min(1).max(320),
|
||||
password: z.string().trim().min(1).max(4096),
|
||||
secure: z.boolean().default(true),
|
||||
host: z.string().trim().min(1).max(255).nullish(),
|
||||
port: z.number().int().min(1).max(65535).nullish(),
|
||||
username: z.string().trim().min(1).max(320).nullish(),
|
||||
password: z.string().trim().min(1).max(4096).nullish(),
|
||||
tls_mode: SmtpTlsModeSchema.nullish(),
|
||||
});
|
||||
|
||||
export type InstanceEmailSmtpTestRequest = z.infer<typeof InstanceEmailSmtpTestRequest>;
|
||||
|
||||
Reference in new issue
Block a user