Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add smtp to frontend #2802

Merged
merged 8 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/startos/src/bins/registry.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ffi::OsString;

use clap::Parser;
use futures::{FutureExt};
use futures::FutureExt;
use tokio::signal::unix::signal;
use tracing::instrument;

Expand Down
4 changes: 2 additions & 2 deletions core/startos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ pub fn server<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"test-smtp",
from_fn_async(system::test_system_smtp)
from_fn_async(system::test_smtp)
.no_display()
.with_about("Send test email using system smtp server and credentials")
.with_about("Send test email using provided smtp server and credentials")
.with_call_remote::<CliContext>()
)
.subcommand(
Expand Down
52 changes: 44 additions & 8 deletions core/startos/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use clap::Parser;
use color_eyre::eyre::eyre;
use futures::FutureExt;
use imbl::vector;
use mail_send::mail_builder::MessageBuilder;
use mail_send::mail_builder::{self, MessageBuilder};
use mail_send::SmtpClientBuilder;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rustls::crypto::CryptoProvider;
Expand Down Expand Up @@ -878,15 +878,33 @@ pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> {
}
Ok(())
}
pub async fn test_system_smtp(
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct TestSmtpParams {
#[arg(long)]
pub server: String,
#[arg(long)]
pub port: u16,
#[arg(long)]
pub from: String,
#[arg(long)]
pub to: String,
#[arg(long)]
pub login: String,
#[arg(long)]
pub password: Option<String>,
}
pub async fn test_smtp(
_: RpcContext,
SmtpValue {
TestSmtpParams {
server,
port,
from,
to,
login,
password,
}: SmtpValue,
}: TestSmtpParams,
) -> Result<(), Error> {
use rustls_pki_types::pem::PemObject;

Expand All @@ -913,17 +931,35 @@ pub async fn test_system_smtp(
);
let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg)
.implicit_tls(false)
.credentials((login.clone().split_once("@").unwrap().0.to_owned(), pass_val));
.credentials((login.split("@").next().unwrap().to_owned(), pass_val));

fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> {
if addr.find("<").map_or(false, |start| {
addr.find(">").map_or(false, |end| start < end)
}) {
addr.split_once("<")
.map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr)))
.unwrap()
.into()
} else {
addr.into()
}
}

let message = MessageBuilder::new()
.from((from.clone(), login.clone()))
.to(vec![(from, login)])
.from(parse_address(&from))
.to(parse_address(&to))
.subject("StartOS Test Email")
.text_body("This is a test email sent from your StartOS Server");
client
.connect()
.await
.map_err(|e| Error::new(eyre!("mail-send connection error: {:?}", e), ErrorKind::Unknown))?
.map_err(|e| {
Error::new(
eyre!("mail-send connection error: {:?}", e),
ErrorKind::Unknown,
)
})?
.send(message)
.await
.map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?;
Expand Down
2 changes: 1 addition & 1 deletion core/startos/src/version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use futures::future::BoxFuture;
use futures::{Future, FutureExt};
use imbl::Vector;
use imbl_value::{to_value, InternedString};
use patch_db::json_ptr::{ ROOT};
use patch_db::json_ptr::ROOT;

use crate::context::RpcContext;
use crate::prelude::*;
Expand Down
4 changes: 2 additions & 2 deletions sdk/base/lib/actions/input/inputSpecConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({
name: "From Address",
required: true,
default: null,
placeholder: "<name>[email protected]",
placeholder: "Example Name <[email protected]>",
inputmode: "email",
patterns: [Patterns.email],
patterns: [Patterns.emailWithName],
}),
login: Value.text({
name: "Login",
Expand Down
10 changes: 10 additions & 0 deletions sdk/base/lib/osBindings/TestSmtpParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type TestSmtpParams = {
server: string
port: number
from: string
to: string
login: string
password: string | null
}
1 change: 1 addition & 0 deletions sdk/base/lib/osBindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export { SignAssetParams } from "./SignAssetParams"
export { SignerInfo } from "./SignerInfo"
export { SmtpValue } from "./SmtpValue"
export { StartStop } from "./StartStop"
export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetPublicParams } from "./UnsetPublicParams"
export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams"
Expand Down
5 changes: 5 additions & 0 deletions sdk/base/lib/util/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export const email: Pattern = {
description: "Must be a valid email address",
}

export const emailWithName: Pattern = {
regex: regexes.emailWithName.source,
description: "Must be a valid email address, optionally with a name",
}

export const base64: Pattern = {
regex: regexes.base64.source,
description:
Expand Down
8 changes: 6 additions & 2 deletions sdk/base/lib/util/regexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ export const torUrl =
// https://ihateregex.io/expr/ascii/
export const ascii = /^[ -~]*$/

//https://ihateregex.io/expr/email/
export const email = /[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/
// https://www.regular-expressions.info/email.html
export const email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/

export const emailWithName = new RegExp(
`(${email.source})|([^<]*<(${email.source})>)`,
)

//https://rgxdb.com/r/1NUN74O6
export const base64 =
Expand Down
28 changes: 27 additions & 1 deletion web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions web/projects/ui/src/app/pages/server-routes/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { TuiInputModule } from '@taiga-ui/kit'
import {
TuiNotificationModule,
TuiTextfieldControllerModule,
} from '@taiga-ui/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { EmailPage } from './email.page'
import { FormModule } from 'src/app/components/form/form.module'
import { IonicModule } from '@ionic/angular'
import { TuiErrorModule, TuiModeModule } from '@taiga-ui/core'
import { TuiAppearanceModule, TuiButtonModule } from '@taiga-ui/experimental'

const routes: Routes = [
{
path: '',
component: EmailPage,
},
]

@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
CommonModule,
FormsModule,
ReactiveFormsModule,
TuiButtonModule,
TuiInputModule,
FormModule,
TuiNotificationModule,
TuiTextfieldControllerModule,
TuiAppearanceModule,
TuiModeModule,
TuiErrorModule,
],
declarations: [EmailPage],
})
export class EmailPageModule {}
70 changes: 70 additions & 0 deletions web/projects/ui/src/app/pages/server-routes/email/email.page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<ion-header>
<ion-toolbar>
<ion-title>Email</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="system"></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<tui-notification>
Fill out the form below to connect to an external SMTP server. With your
permission, installed services can use the SMTP server to send emails. To
grant permission to a particular service, visit that service's "Actions"
page. Not all services support sending emails.
<a
href="https://docs.start9.com/latest/user-manual/0.3.5.x/smtp"
target="_blank"
rel="noreferrer"
>
View instructions
</a>
</tui-notification>
<ng-container *ngIf="form$ | async as form">
<form [formGroup]="form" [style.text-align]="'right'">
<h3 class="g-title">SMTP Credentials</h3>
<form-group
*ngIf="spec | async as resolved"
[spec]="resolved"
></form-group>
<button
*ngIf="isSaved"
tuiButton
appearance="destructive"
[style.margin-top.rem]="1"
[style.margin-right.rem]="1"
(click)="save(null)"
>
Delete
</button>
<button
tuiButton
[style.margin-top.rem]="1"
[disabled]="form.invalid"
(click)="save(form.value)"
>
Save
</button>
</form>
<form [style.text-align]="'right'">
<h3 class="g-title">Send Test Email</h3>
<tui-input
[(ngModel)]="testAddress"
[ngModelOptions]="{ standalone: true }"
>
To Address
<input tuiTextfield inputmode="email" />
</tui-input>
<button
tuiButton
appearance="secondary"
[style.margin-top.rem]="1"
[disabled]="!testAddress || form.invalid"
(click)="sendTestEmail(form.value)"
>
Send
</button>
</form>
</ng-container>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
form {
padding-top: 24px;
margin: auto;
max-width: 30rem;
}

h3 {
display: flex;
}
Loading