mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
add smtp to frontend (#2802)
* add smtp to frontend * left align headers * just email * change all to email * fix test-smtp api * types * fix email from and login address handling --------- Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
form {
|
||||
padding-top: 24px;
|
||||
margin: auto;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { IST, inputSpec } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { switchMap, tap } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
|
||||
@Component({
|
||||
selector: 'email-page',
|
||||
templateUrl: './email.page.html',
|
||||
styleUrls: ['./email.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class EmailPage {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
isSaved = false
|
||||
testAddress = ''
|
||||
|
||||
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
||||
inputSpec.constants.customSmtp,
|
||||
)
|
||||
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
|
||||
tap(value => (this.isSaved = !!value)),
|
||||
switchMap(async value =>
|
||||
this.formService.createForm(await this.spec, value),
|
||||
),
|
||||
)
|
||||
|
||||
async save(
|
||||
value: typeof inputSpec.constants.customSmtp._TYPE | null,
|
||||
): Promise<void> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
if (value) {
|
||||
await this.api.setSmtp(value)
|
||||
this.isSaved = true
|
||||
} else {
|
||||
await this.api.clearSmtp({})
|
||||
this.isSaved = false
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
|
||||
const loader = this.loader.open('Sending email...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.testSmtp({
|
||||
to: this.testAddress,
|
||||
...value,
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open(
|
||||
`A test email has been sent to ${this.testAddress}.<br /><br /><b>Check your spam folder and mark as not spam</b>`,
|
||||
{
|
||||
label: 'Success',
|
||||
size: 's',
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -76,10 +76,15 @@ const routes: Routes = [
|
||||
import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule),
|
||||
},
|
||||
{
|
||||
path: 'wireless',
|
||||
path: 'wifi',
|
||||
loadChildren: () =>
|
||||
import('./wifi/wifi.module').then(m => m.WifiPageModule),
|
||||
},
|
||||
{
|
||||
path: 'email',
|
||||
loadChildren: () =>
|
||||
import('./email/email.module').then(m => m.EmailPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -463,6 +463,15 @@ export class ServerShowPage {
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
description: 'Connect to an external SMTP server for sending emails',
|
||||
icon: 'mail-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['email'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
description:
|
||||
@@ -474,12 +483,12 @@ export class ServerShowPage {
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Wireless',
|
||||
title: 'WiFi',
|
||||
description:
|
||||
'Connect your server to WiFi instead of Ethernet (not recommended)',
|
||||
icon: 'wifi',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['wireless'], {
|
||||
this.navCtrl.navigateForward(['wifi'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="system"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Wireless Settings</ion-title>
|
||||
<ion-title>WiFi Settings</ion-title>
|
||||
<ion-buttons slot="end" *ngIf="hasWifi$ | async">
|
||||
<ion-button (click)="getWifi()">
|
||||
Refresh
|
||||
|
||||
@@ -102,6 +102,17 @@ export module RR {
|
||||
} // net.tor.reset
|
||||
export type ResetTorRes = null
|
||||
|
||||
// smtp
|
||||
|
||||
export type SetSMTPReq = T.SmtpValue // server.set-smtp
|
||||
export type SetSMTPRes = null
|
||||
|
||||
export type ClearSMTPReq = {} // server.clear-smtp
|
||||
export type ClearSMTPRes = null
|
||||
|
||||
export type TestSMTPReq = SetSMTPReq & { to: string } // server.test-smtp
|
||||
export type TestSMTPRes = null
|
||||
|
||||
// sessions
|
||||
|
||||
export type GetSessionsReq = {} // sessions.list
|
||||
|
||||
@@ -128,6 +128,14 @@ export abstract class ApiService {
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
// smtp
|
||||
|
||||
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
||||
|
||||
abstract clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes>
|
||||
|
||||
abstract testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes>
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
abstract registryRequest<T>(
|
||||
|
||||
@@ -382,6 +382,20 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
// smtp
|
||||
|
||||
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.set-smtp', params })
|
||||
}
|
||||
|
||||
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.clear-smtp', params })
|
||||
}
|
||||
|
||||
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.test-smtp', params })
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
|
||||
@@ -557,6 +557,41 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// smtp
|
||||
|
||||
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/smtp',
|
||||
value: params,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/smtp',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
|
||||
Reference in New Issue
Block a user