mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
rework smtp
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/SmtpSecurity.ts
Normal file
3
sdk/base/lib/osBindings/SmtpSecurity.ts
Normal 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'
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user