Skip to content

Commit c63cd03

Browse files
committed
add smtp to frontend
1 parent e9d851e commit c63cd03

File tree

12 files changed

+314
-5
lines changed

12 files changed

+314
-5
lines changed

web/package-lock.json

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/projects/ui/src/app/pages/server-routes/server-routing.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,15 @@ const routes: Routes = [
7676
import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule),
7777
},
7878
{
79-
path: 'wireless',
79+
path: 'wifi',
8080
loadChildren: () =>
8181
import('./wifi/wifi.module').then(m => m.WifiPageModule),
8282
},
83+
{
84+
path: 'smtp',
85+
loadChildren: () =>
86+
import('./smtp/smtp.module').then(m => m.SMTPPageModule),
87+
},
8388
]
8489

8590
@NgModule({

web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,16 @@ export class ServerShowPage {
463463
detail: true,
464464
disabled$: of(false),
465465
},
466+
{
467+
title: 'SMTP (email)',
468+
description:
469+
'Connect to an external SMTP server to send yourself emails',
470+
icon: 'mail-outline',
471+
action: () =>
472+
this.navCtrl.navigateForward(['smtp'], { relativeTo: this.route }),
473+
detail: true,
474+
disabled$: of(false),
475+
},
466476
{
467477
title: 'SSH',
468478
description:
@@ -474,12 +484,12 @@ export class ServerShowPage {
474484
disabled$: of(false),
475485
},
476486
{
477-
title: 'Wireless',
487+
title: 'WiFi',
478488
description:
479489
'Connect your server to WiFi instead of Ethernet (not recommended)',
480490
icon: 'wifi',
481491
action: () =>
482-
this.navCtrl.navigateForward(['wireless'], {
492+
this.navCtrl.navigateForward(['wifi'], {
483493
relativeTo: this.route,
484494
}),
485495
detail: true,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { NgModule } from '@angular/core'
2+
import { CommonModule } from '@angular/common'
3+
import { Routes, RouterModule } from '@angular/router'
4+
import { TuiInputModule } from '@taiga-ui/kit'
5+
import {
6+
TuiNotificationModule,
7+
TuiTextfieldControllerModule,
8+
} from '@taiga-ui/core'
9+
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
10+
import { SMTPPage } from './smtp.page'
11+
import { FormModule } from 'src/app/components/form/form.module'
12+
import { IonicModule } from '@ionic/angular'
13+
import { TuiErrorModule, TuiModeModule } from '@taiga-ui/core'
14+
import { TuiAppearanceModule, TuiButtonModule } from '@taiga-ui/experimental'
15+
16+
const routes: Routes = [
17+
{
18+
path: '',
19+
component: SMTPPage,
20+
},
21+
]
22+
23+
@NgModule({
24+
imports: [
25+
CommonModule,
26+
IonicModule,
27+
RouterModule.forChild(routes),
28+
CommonModule,
29+
FormsModule,
30+
ReactiveFormsModule,
31+
TuiButtonModule,
32+
TuiInputModule,
33+
FormModule,
34+
TuiNotificationModule,
35+
TuiTextfieldControllerModule,
36+
TuiAppearanceModule,
37+
TuiModeModule,
38+
TuiErrorModule,
39+
],
40+
declarations: [SMTPPage],
41+
})
42+
export class SMTPPageModule {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<ion-header>
2+
<ion-toolbar>
3+
<ion-title>SMTP (email)</ion-title>
4+
<ion-buttons slot="start">
5+
<ion-back-button defaultHref="system"></ion-back-button>
6+
</ion-buttons>
7+
</ion-toolbar>
8+
</ion-header>
9+
10+
<ion-content class="ion-padding">
11+
<tui-notification>
12+
Fill out the form below to connect to an external SMTP server. With your
13+
permission, installed services can use the SMTP server to send emails. To
14+
grant permission to a particular service, visit that service's "Actions"
15+
page. Not all services support sending emails.
16+
<a
17+
href="https://docs.start9.com/latest/user-manual/0.3.5.x/smtp"
18+
target="_blank"
19+
rel="noreferrer"
20+
>
21+
View instructions
22+
</a>
23+
</tui-notification>
24+
<ng-container *ngIf="form$ | async as form">
25+
<form [formGroup]="form" [style.text-align]="'right'">
26+
<h3 class="g-title">SMTP Credentials</h3>
27+
<form-group
28+
*ngIf="spec | async as resolved"
29+
[spec]="resolved"
30+
></form-group>
31+
<button
32+
*ngIf="isSaved"
33+
tuiButton
34+
appearance="destructive"
35+
[style.margin-top.rem]="1"
36+
[style.margin-right.rem]="1"
37+
(click)="save(null)"
38+
>
39+
Delete
40+
</button>
41+
<button
42+
tuiButton
43+
[style.margin-top.rem]="1"
44+
[disabled]="form.invalid"
45+
(click)="save(form.value)"
46+
>
47+
Save
48+
</button>
49+
</form>
50+
<form [style.text-align]="'right'">
51+
<h3 class="g-title">Send Test Email</h3>
52+
<tui-input
53+
[(ngModel)]="testAddress"
54+
[ngModelOptions]="{ standalone: true }"
55+
>
56+
To Address
57+
<input tuiTextfield inputmode="email" />
58+
</tui-input>
59+
<button
60+
tuiButton
61+
appearance="secondary"
62+
[style.margin-top.rem]="1"
63+
[disabled]="!testAddress || form.invalid"
64+
(click)="sendTestEmail(form.value)"
65+
>
66+
Send
67+
</button>
68+
</form>
69+
</ng-container>
70+
</ion-content>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
form {
2+
padding-top: 24px;
3+
margin: auto;
4+
max-width: 30rem;
5+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
2+
import { ErrorService, LoadingService } from '@start9labs/shared'
3+
import { IST, inputSpec } from '@start9labs/start-sdk'
4+
import { TuiDialogService } from '@taiga-ui/core'
5+
import { PatchDB } from 'patch-db-client'
6+
import { switchMap, tap } from 'rxjs'
7+
import { ApiService } from 'src/app/services/api/embassy-api.service'
8+
import { FormService } from 'src/app/services/form.service'
9+
import { DataModel } from 'src/app/services/patch-db/data-model'
10+
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
11+
12+
@Component({
13+
selector: 'smtp-page',
14+
templateUrl: './smtp.page.html',
15+
styleUrls: ['./smtp.page.scss'],
16+
changeDetection: ChangeDetectionStrategy.OnPush,
17+
})
18+
export class SMTPPage {
19+
private readonly dialogs = inject(TuiDialogService)
20+
private readonly loader = inject(LoadingService)
21+
private readonly errorService = inject(ErrorService)
22+
private readonly formService = inject(FormService)
23+
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
24+
private readonly api = inject(ApiService)
25+
26+
isSaved = false
27+
testAddress = ''
28+
29+
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
30+
inputSpec.constants.customSmtp,
31+
)
32+
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
33+
tap(value => (this.isSaved = !!value)),
34+
switchMap(async value =>
35+
this.formService.createForm(await this.spec, value),
36+
),
37+
)
38+
39+
async save(
40+
value: typeof inputSpec.constants.customSmtp._TYPE | null,
41+
): Promise<void> {
42+
const loader = this.loader.open('Saving...').subscribe()
43+
44+
try {
45+
if (value) {
46+
await this.api.setSmtp(value)
47+
this.isSaved = true
48+
} else {
49+
await this.api.clearSmtp({})
50+
this.isSaved = false
51+
}
52+
} catch (e: any) {
53+
this.errorService.handleError(e)
54+
} finally {
55+
loader.unsubscribe()
56+
}
57+
}
58+
59+
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
60+
const loader = this.loader.open('Sending email...').subscribe()
61+
62+
try {
63+
await this.api.testSmtp({
64+
to: this.testAddress,
65+
...value,
66+
})
67+
} catch (e: any) {
68+
this.errorService.handleError(e)
69+
} finally {
70+
loader.unsubscribe()
71+
}
72+
73+
this.dialogs
74+
.open(
75+
`A test email has been sent to ${this.testAddress}.<br /><br /><b>Check your spam folder and mark as not spam</b>`,
76+
{
77+
label: 'Success',
78+
size: 's',
79+
},
80+
)
81+
.subscribe()
82+
}
83+
}

web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<ion-buttons slot="start">
44
<ion-back-button defaultHref="system"></ion-back-button>
55
</ion-buttons>
6-
<ion-title>Wireless Settings</ion-title>
6+
<ion-title>WiFi Settings</ion-title>
77
<ion-buttons slot="end" *ngIf="hasWifi$ | async">
88
<ion-button (click)="getWifi()">
99
Refresh

web/projects/ui/src/app/services/api/api.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ export module RR {
102102
} // net.tor.reset
103103
export type ResetTorRes = null
104104

105+
// smtp
106+
107+
export type SetSMTPReq = T.SmtpValue // server.set-smtp
108+
export type SetSMTPRes = null
109+
110+
export type ClearSMTPReq = {} // server.clear-smtp
111+
export type ClearSMTPRes = null
112+
113+
export type TestSMTPReq = SetSMTPReq & { to: string } // server.test-smtp
114+
export type TestSMTPRes = null
115+
105116
// sessions
106117

107118
export type GetSessionsReq = {} // sessions.list

web/projects/ui/src/app/services/api/embassy-api.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ export abstract class ApiService {
127127

128128
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
129129

130+
// smtp
131+
132+
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
133+
134+
abstract clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes>
135+
136+
abstract testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes>
137+
130138
// marketplace URLs
131139

132140
abstract registryRequest<T>(

web/projects/ui/src/app/services/api/embassy-live-api.service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,20 @@ export class LiveApiService extends ApiService {
382382
return this.rpcRequest({ method: 'wifi.delete', params })
383383
}
384384

385+
// smtp
386+
387+
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
388+
return this.rpcRequest({ method: 'server.set-smtp', params })
389+
}
390+
391+
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
392+
return this.rpcRequest({ method: 'server.clear-smtp', params })
393+
}
394+
395+
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
396+
return this.rpcRequest({ method: 'server.test-smtp', params })
397+
}
398+
385399
// ssh
386400

387401
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {

web/projects/ui/src/app/services/api/embassy-mock-api.service.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,41 @@ export class MockApiService extends ApiService {
556556
return null
557557
}
558558

559+
// smtp
560+
561+
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
562+
await pauseFor(2000)
563+
const patch = [
564+
{
565+
op: PatchOp.REPLACE,
566+
path: '/serverInfo/smtp',
567+
value: params,
568+
},
569+
]
570+
this.mockRevision(patch)
571+
572+
return null
573+
}
574+
575+
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
576+
await pauseFor(2000)
577+
const patch = [
578+
{
579+
op: PatchOp.REPLACE,
580+
path: '/serverInfo/smtp',
581+
value: null,
582+
},
583+
]
584+
this.mockRevision(patch)
585+
586+
return null
587+
}
588+
589+
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
590+
await pauseFor(2000)
591+
return null
592+
}
593+
559594
// ssh
560595

561596
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {

0 commit comments

Comments
 (0)