rework smtp

This commit is contained in:
Matt Hill
2026-02-23 14:25:51 -07:00
parent 804560d43c
commit e9b9925c0e
14 changed files with 333 additions and 107 deletions

View File

@@ -3124,7 +3124,7 @@ help.arg.smtp-from:
fr_FR: "Adresse de l'expéditeur" fr_FR: "Adresse de l'expéditeur"
pl_PL: "Adres nadawcy e-mail" pl_PL: "Adres nadawcy e-mail"
help.arg.smtp-login: help.arg.smtp-username:
en_US: "SMTP authentication username" en_US: "SMTP authentication username"
de_DE: "SMTP-Authentifizierungsbenutzername" de_DE: "SMTP-Authentifizierungsbenutzername"
es_ES: "Nombre de usuario de autenticación SMTP" es_ES: "Nombre de usuario de autenticación SMTP"
@@ -3145,13 +3145,20 @@ help.arg.smtp-port:
fr_FR: "Port du serveur SMTP" fr_FR: "Port du serveur SMTP"
pl_PL: "Port serwera SMTP" pl_PL: "Port serwera SMTP"
help.arg.smtp-server: help.arg.smtp-host:
en_US: "SMTP server hostname" en_US: "SMTP server hostname"
de_DE: "SMTP-Server-Hostname" de_DE: "SMTP-Server-Hostname"
es_ES: "Nombre de host del servidor SMTP" es_ES: "Nombre de host del servidor SMTP"
fr_FR: "Nom d'hôte du serveur SMTP" fr_FR: "Nom d'hôte du serveur SMTP"
pl_PL: "Nazwa hosta serwera SMTP" pl_PL: "Nazwa hosta serwera SMTP"
help.arg.smtp-security:
en_US: "Connection security mode (starttls or tls)"
de_DE: "Verbindungssicherheitsmodus (starttls oder tls)"
es_ES: "Modo de seguridad de conexión (starttls o tls)"
fr_FR: "Mode de sécurité de connexion (starttls ou tls)"
pl_PL: "Tryb zabezpieczeń połączenia (starttls lub tls)"
help.arg.smtp-to: help.arg.smtp-to:
en_US: "Email recipient address" en_US: "Email recipient address"
de_DE: "E-Mail-Empfängeradresse" de_DE: "E-Mail-Empfängeradresse"

View File

@@ -1049,20 +1049,36 @@ async fn get_disk_info() -> Result<MetricsDisk, Error> {
}) })
} }
#[derive(
Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, TS, clap::ValueEnum,
)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum SmtpSecurity {
#[default]
Starttls,
Tls,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SmtpValue { pub struct SmtpValue {
#[arg(long, help = "help.arg.smtp-server")] #[arg(long, help = "help.arg.smtp-host")]
pub server: String, #[serde(alias = "server")]
pub host: String,
#[arg(long, help = "help.arg.smtp-port")] #[arg(long, help = "help.arg.smtp-port")]
pub port: u16, pub port: u16,
#[arg(long, help = "help.arg.smtp-from")] #[arg(long, help = "help.arg.smtp-from")]
pub from: String, pub from: String,
#[arg(long, help = "help.arg.smtp-login")] #[arg(long, help = "help.arg.smtp-username")]
pub login: String, #[serde(alias = "login")]
pub username: String,
#[arg(long, help = "help.arg.smtp-password")] #[arg(long, help = "help.arg.smtp-password")]
pub password: Option<String>, pub password: Option<String>,
#[arg(long, help = "help.arg.smtp-security")]
#[serde(default)]
pub security: SmtpSecurity,
} }
pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> { pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> {
let smtp = Some(smtp); let smtp = Some(smtp);
@@ -1121,47 +1137,63 @@ pub async fn set_ifconfig_url(
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TestSmtpParams { pub struct TestSmtpParams {
#[arg(long, help = "help.arg.smtp-server")] #[arg(long, help = "help.arg.smtp-host")]
pub server: String, pub host: String,
#[arg(long, help = "help.arg.smtp-port")] #[arg(long, help = "help.arg.smtp-port")]
pub port: u16, pub port: u16,
#[arg(long, help = "help.arg.smtp-from")] #[arg(long, help = "help.arg.smtp-from")]
pub from: String, pub from: String,
#[arg(long, help = "help.arg.smtp-to")] #[arg(long, help = "help.arg.smtp-to")]
pub to: String, pub to: String,
#[arg(long, help = "help.arg.smtp-login")] #[arg(long, help = "help.arg.smtp-username")]
pub login: String, pub username: String,
#[arg(long, help = "help.arg.smtp-password")] #[arg(long, help = "help.arg.smtp-password")]
pub password: String, pub password: String,
#[arg(long, help = "help.arg.smtp-security")]
#[serde(default)]
pub security: SmtpSecurity,
} }
pub async fn test_smtp( pub async fn test_smtp(
_: RpcContext, _: RpcContext,
TestSmtpParams { TestSmtpParams {
server, host,
port, port,
from, from,
to, to,
login, username,
password, password,
security,
}: TestSmtpParams, }: TestSmtpParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
use lettre::message::header::ContentType; use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::client::{Tls, TlsParameters};
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
AsyncSmtpTransport::<Tokio1Executor>::relay(&server)? let creds = Credentials::new(username, password);
.port(port) let message = Message::builder()
.credentials(Credentials::new(login, password)) .from(from.parse()?)
.build() .to(to.parse()?)
.send( .subject("StartOS Test Email")
Message::builder() .header(ContentType::TEXT_PLAIN)
.from(from.parse()?) .body("This is a test email sent from your StartOS Server".to_owned())?;
.to(to.parse()?)
.subject("StartOS Test Email") let transport = match security {
.header(ContentType::TEXT_PLAIN) SmtpSecurity::Starttls => AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
.body("This is a test email sent from your StartOS Server".to_owned())?, .port(port)
) .credentials(creds)
.await?; .build(),
SmtpSecurity::Tls => {
let tls = TlsParameters::new(host.clone())?;
AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
.port(port)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.build()
}
};
transport.send(message).await?;
Ok(()) Ok(())
} }

View File

@@ -166,6 +166,9 @@ impl VersionT for Version {
// Rebuild from actual assigned ports in all bindings // Rebuild from actual assigned ports in all bindings
migrate_available_ports(db); migrate_available_ports(db);
// Migrate SMTP: rename server->host, login->username, add security field
migrate_smtp(db);
Ok(migration_data) Ok(migration_data)
} }
@@ -242,6 +245,25 @@ fn migrate_available_ports(db: &mut Value) {
} }
} }
fn migrate_smtp(db: &mut Value) {
if let Some(smtp) = db
.get_mut("public")
.and_then(|p| p.get_mut("serverInfo"))
.and_then(|s| s.get_mut("smtp"))
.and_then(|s| s.as_object_mut())
{
if let Some(server) = smtp.remove("server") {
smtp.insert("host".into(), server);
}
if let Some(login) = smtp.remove("login") {
smtp.insert("username".into(), login);
}
if !smtp.contains_key("security") {
smtp.insert("security".into(), json!("starttls"));
}
}
}
fn migrate_host(host: Option<&mut Value>) { fn migrate_host(host: Option<&mut Value>) {
let Some(host) = host.and_then(|h| h.as_object_mut()) else { let Some(host) = host.and_then(|h| h.as_object_mut()) else {
return; return;

View File

@@ -5,42 +5,124 @@ import { Value } from './builder/value'
import { Variants } from './builder/variants' import { Variants } from './builder/variants'
/** /**
* Base SMTP settings, to be used by StartOS for system wide SMTP * Creates an SMTP field spec with provider-specific defaults pre-filled.
*/ */
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of< function smtpFields(
InputSpecOf<SmtpValue> defaults: {
>({ host?: string
server: Value.text({ port?: number
name: 'SMTP Server', security?: 'starttls' | 'tls'
required: true, } = {},
default: null, ): InputSpec<SmtpValue> {
}), return InputSpec.of<InputSpecOf<SmtpValue>>({
port: Value.number({ host: Value.text({
name: 'Port', name: 'Host',
required: true, required: true,
default: 587, default: defaults.host ?? null,
min: 1, placeholder: 'smtp.example.com',
max: 65535, }),
integer: true, port: Value.number({
}), name: 'Port',
from: Value.text({ required: true,
name: 'From Address', default: defaults.port ?? 587,
required: true, min: 1,
default: null, max: 65535,
placeholder: 'Example Name <test@example.com>', integer: true,
inputmode: 'email', }),
patterns: [Patterns.emailWithName], security: Value.select({
}), name: 'Connection Security',
login: Value.text({ default: defaults.security ?? 'starttls',
name: 'Login', values: {
required: true, starttls: 'STARTTLS',
default: null, tls: 'TLS',
}), },
password: Value.text({ }),
name: 'Password', from: Value.text({
required: false, name: 'From Address',
default: null, required: true,
masked: true, default: null,
placeholder: 'Example Name <test@example.com>',
patterns: [Patterns.emailWithName],
}),
username: Value.text({
name: 'Username',
required: true,
default: null,
}),
password: Value.text({
name: 'Password',
required: false,
default: null,
masked: true,
}),
})
}
/**
* Base SMTP settings with no provider-specific defaults.
*/
export const customSmtp = smtpFields()
/**
* Provider presets for SMTP configuration.
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
*/
export const smtpProviderVariants = Variants.of({
gmail: {
name: 'Gmail',
spec: smtpFields({
host: 'smtp.gmail.com',
port: 587,
security: 'starttls',
}),
},
ses: {
name: 'Amazon SES',
spec: smtpFields({
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
security: 'starttls',
}),
},
sendgrid: {
name: 'SendGrid',
spec: smtpFields({
host: 'smtp.sendgrid.net',
port: 587,
security: 'starttls',
}),
},
mailgun: {
name: 'Mailgun',
spec: smtpFields({
host: 'smtp.mailgun.org',
port: 587,
security: 'starttls',
}),
},
protonmail: {
name: 'Proton Mail',
spec: smtpFields({
host: 'smtp.protonmail.ch',
port: 587,
security: 'starttls',
}),
},
other: {
name: 'Other',
spec: customSmtp,
},
})
/**
* System SMTP settings with provider presets.
* Wraps smtpProviderVariants in a union for use by the system email settings page.
*/
export const systemSmtpSpec = InputSpec.of({
provider: Value.union({
name: 'Provider',
default: null as any,
variants: smtpProviderVariants,
}), }),
}) })
@@ -55,19 +137,24 @@ const smtpVariants = Variants.of({
'A custom from address for this service. If not provided, the system from address will be used.', 'A custom from address for this service. If not provided, the system from address will be used.',
required: false, required: false,
default: null, default: null,
placeholder: '<name>test@example.com', placeholder: 'Name <test@example.com>',
inputmode: 'email', patterns: [Patterns.emailWithName],
patterns: [Patterns.email],
}), }),
}), }),
}, },
custom: { custom: {
name: 'Custom Credentials', name: 'Custom Credentials',
spec: customSmtp, spec: InputSpec.of({
provider: Value.union({
name: 'Provider',
default: null as any,
variants: smtpProviderVariants,
}),
}),
}, },
}) })
/** /**
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings * For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings with provider presets
*/ */
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => { export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once() const smtp = await new GetSystemSmtp(effects).once()

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SmtpSecurity = 'starttls' | 'tls'

View File

@@ -1,9 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SmtpSecurity } from './SmtpSecurity'
export type SmtpValue = { export type SmtpValue = {
server: string host: string
port: number port: number
from: string from: string
login: string username: string
password: string | null password: string | null
security: SmtpSecurity
} }

View File

@@ -1,10 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SmtpSecurity } from './SmtpSecurity'
export type TestSmtpParams = { export type TestSmtpParams = {
server: string host: string
port: number port: number
from: string from: string
to: string to: string
login: string username: string
password: string password: string
security: SmtpSecurity
} }

View File

@@ -269,6 +269,7 @@ export { SideloadResponse } from './SideloadResponse'
export { SignalStrength } from './SignalStrength' export { SignalStrength } from './SignalStrength'
export { SignAssetParams } from './SignAssetParams' export { SignAssetParams } from './SignAssetParams'
export { SignerInfo } from './SignerInfo' export { SignerInfo } from './SignerInfo'
export { SmtpSecurity } from './SmtpSecurity'
export { SmtpValue } from './SmtpValue' export { SmtpValue } from './SmtpValue'
export { SshAddParams } from './SshAddParams' export { SshAddParams } from './SshAddParams'
export { SshDeleteParams } from './SshDeleteParams' export { SshDeleteParams } from './SshDeleteParams'

View File

@@ -115,11 +115,12 @@ export type Daemon = {
export type HealthStatus = NamedHealthCheckResult['result'] export type HealthStatus = NamedHealthCheckResult['result']
/** SMTP mail server configuration values. */ /** SMTP mail server configuration values. */
export type SmtpValue = { export type SmtpValue = {
server: string host: string
port: number port: number
from: string from: string
login: string username: string
password: string | null | undefined password: string | null | undefined
security: 'starttls' | 'tls'
} }
/** /**

View File

@@ -9,7 +9,12 @@ import {
import { ServiceInterfaceType, Effects } from '../../base/lib/types' import { ServiceInterfaceType, Effects } from '../../base/lib/types'
import * as patterns from '../../base/lib/util/patterns' import * as patterns from '../../base/lib/util/patterns'
import { Backups } from './backup/Backups' import { Backups } from './backup/Backups'
import { smtpInputSpec } from '../../base/lib/actions/input/inputSpecConstants' import {
smtpInputSpec,
systemSmtpSpec,
customSmtp,
smtpProviderVariants,
} from '../../base/lib/actions/input/inputSpecConstants'
import { Daemon, Daemons } from './mainFn/Daemons' import { Daemon, Daemons } from './mainFn/Daemons'
import { checkPortListening } from './health/checkFns/checkPortListening' import { checkPortListening } from './health/checkFns/checkPortListening'
import { checkWebUrl, runHealthScript } from './health/checkFns' import { checkWebUrl, runHealthScript } from './health/checkFns'
@@ -468,7 +473,12 @@ export class StartSdk<Manifest extends T.SDKManifest> {
run: Run<{}>, run: Run<{}>,
) => Action.withoutInput(id, metadata, run), ) => Action.withoutInput(id, metadata, run),
}, },
inputSpecConstants: { smtpInputSpec }, inputSpecConstants: {
smtpInputSpec,
systemSmtpSpec,
customSmtp,
smtpProviderVariants,
},
/** /**
* @description Use this function to create a service interface. * @description Use this function to create a service interface.
* @param effects * @param effects

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.51", "version": "0.4.0-beta.52",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.51", "version": "0.4.0-beta.52",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.51", "version": "0.4.0-beta.52",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",

View File

@@ -116,19 +116,6 @@ export default class ServiceAboutRoute {
}, },
], ],
}, },
{
header: 'Source Code',
items: [
{
name: 'Upstream service',
value: manifest.upstreamRepo,
},
{
name: 'StartOS package',
value: manifest.packageRepo,
},
],
},
{ {
header: 'Links', header: 'Links',
items: [ items: [
@@ -146,6 +133,19 @@ export default class ServiceAboutRoute {
}, },
], ],
}, },
{
header: 'Source Code',
items: [
{
name: 'Upstream service',
value: manifest.upstreamRepo,
},
{
name: 'StartOS package',
value: manifest.packageRepo,
},
],
},
] ]
}), }),
), ),

View File

@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { import {
@@ -10,11 +15,11 @@ import {
i18nPipe, i18nPipe,
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { inputSpec, IST } from '@start9labs/start-sdk' import { inputSpec } from '@start9labs/start-sdk'
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core' import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout' import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { switchMap, tap } from 'rxjs' import { Subscription, switchMap, tap } from 'rxjs'
import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component' import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormService } from 'src/app/services/form.service' import { FormService } from 'src/app/services/form.service'
@@ -22,6 +27,32 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
const PROVIDER_HINTS: Record<string, string> = {
gmail:
'Requires an App Password. Enable 2FA in your Google account, then generate an App Password.',
ses: 'Use SMTP credentials (not IAM credentials). Update the host to match your SES region.',
sendgrid:
"Username is 'apikey' (literal). Password is your SendGrid API key.",
mailgun: 'Use SMTP credentials from your Mailgun domain settings.',
protonmail:
'Requires a Proton for Business account. Use your Proton email as username.',
}
function detectProviderKey(host: string | undefined): string {
if (!host) return 'other'
const providers: Record<string, string> = {
'smtp.gmail.com': 'gmail',
'smtp.sendgrid.net': 'sendgrid',
'smtp.mailgun.org': 'mailgun',
'smtp.protonmail.ch': 'protonmail',
}
for (const [h, key] of Object.entries(providers)) {
if (host === h) return key
}
if (host.endsWith('.amazonaws.com')) return 'ses'
return 'other'
}
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
@@ -52,6 +83,9 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
@if (spec | async; as resolved) { @if (spec | async; as resolved) {
<form-group [spec]="resolved" /> <form-group [spec]="resolved" />
} }
@if (providerHint()) {
<p class="provider-hint">{{ providerHint() }}</p>
}
<footer> <footer>
@if (isSaved) { @if (isSaved) {
<button <button
@@ -116,6 +150,12 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
footer { footer {
justify-content: flex-end; justify-content: flex-end;
} }
.provider-hint {
margin: 0.5rem 0 0;
font-size: 0.85rem;
opacity: 0.7;
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
@@ -142,27 +182,45 @@ export default class SystemEmailComponent {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
readonly providerHint = signal('')
private providerSub: Subscription | null = null
testAddress = '' testAddress = ''
isSaved = false isSaved = false
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec( readonly spec = configBuilderToSpec(inputSpec.constants.systemSmtpSpec)
inputSpec.constants.customSmtp,
)
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe( readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
tap(value => (this.isSaved = !!value)), tap(value => {
switchMap(async value => this.isSaved = !!value
this.formService.createForm(await this.spec, value), }),
), switchMap(async value => {
const spec = await this.spec
const formData = value
? { provider: { selection: detectProviderKey(value.host), value } }
: undefined
const form = this.formService.createForm(spec, formData)
// Watch provider selection for hints
this.providerSub?.unsubscribe()
const selectionCtrl = form.get('provider.selection')
if (selectionCtrl) {
this.providerHint.set(PROVIDER_HINTS[selectionCtrl.value] || '')
this.providerSub = selectionCtrl.valueChanges.subscribe(key => {
this.providerHint.set(PROVIDER_HINTS[key] || '')
})
}
return form
}),
) )
async save( async save(formValue: Record<string, any> | null): Promise<void> {
value: typeof inputSpec.constants.customSmtp._TYPE | null,
): Promise<void> {
const loader = this.loader.open('Saving').subscribe() const loader = this.loader.open('Saving').subscribe()
try { try {
if (value) { if (formValue) {
await this.api.setSmtp(value) await this.api.setSmtp(formValue['provider'].value)
this.isSaved = true this.isSaved = true
} else { } else {
await this.api.clearSmtp({}) await this.api.clearSmtp({})
@@ -175,15 +233,16 @@ export default class SystemEmailComponent {
} }
} }
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) { async sendTestEmail(formValue: Record<string, any>) {
const smtpValue = formValue['provider'].value
const loader = this.loader.open('Sending email').subscribe() const loader = this.loader.open('Sending email').subscribe()
const success = const success =
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}. <i>${this.i18n.transform('Check your spam folder and mark as not spam.')}</i>` as i18nKey `${this.i18n.transform('A test email has been sent to')} ${this.testAddress}. <i>${this.i18n.transform('Check your spam folder and mark as not spam.')}</i>` as i18nKey
try { try {
await this.api.testSmtp({ await this.api.testSmtp({
...value, ...smtpValue,
password: value.password || '', password: smtpValue.password || '',
to: this.testAddress, to: this.testAddress,
}) })
this.dialog this.dialog