Files
zitadel/console/src/app/modules/smtp-provider/smtp-provider.component.html
T
RamonandGitHub ceffb9c536 feat: SMTP xoauth2 in console (#11545)
# Which Problems Are Solved
This adds the newly added xoauth2 option to the console allowing SMTP
Providers to be setup that use xoauth2 instead of plain auth.

# How the Problems Are Solved
Added the xoauth2 option to the SMTP Provider Settings


# Additional Changes
Heavily refactored the SMTP Provider page to allow more dynamic
configuration and use Tanstack Query for Data Fetching.

# Additional Context
- Closes #11451
- Backend changes #11239
2026-02-11 17:12:41 +00:00

346 lines
14 KiB
HTML

@let state = this.state();
@let stepper = this.stepper();
<cnsl-create-layout
[title]="
!state || 'config' in state
? ('SMTP.DETAIL.TITLE' | translate)
: ('SMTP.CREATE.STEPS.TITLE' | translate: { value: state.mainForm.controls.description.value })
"
[createSteps]="4"
[currentCreateStep]="stepper ? stepper.selectedIndex + 1 : 1"
(closed)="location.back()"
>
<div class="wizard-header">
@if (state) {
@if ('defaults' in state) {
<img class="smtp-logo" [src]="state.defaults.image" [alt]="state.defaults.description" />
}
@if (!('defaults' in state) && !('config' in state)) {
<div class="smtp-icon">
<mat-icon class="icon" svgIcon="mdi_smtp" alt="providerDefaultSetting.name" />
</div>
}
}
<h2>
{{
!state || !('defaults' in state)
? ('SMTP.CREATE.STEPS.CURRENT_DESC_TITLE' | translate)
: ('SMTP.CREATE.STEPS.CREATE_DESC_TITLE' | translate: { value: state.defaults.description | titlecase })
}}
</h2>
</div>
@if (configOrDefaultsQuery.isLoading()) {
<mat-progress-bar class="progress-bar" color="primary" mode="indeterminate" />
}
@if (state) {
<mat-horizontal-stepper
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
[selectedIndex]="preselectedStep()"
linear
#stepper
labelPosition="bottom"
>
<mat-step [editable]="true">
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.PROVIDER_SETTINGS' | translate }}</ng-template>
<form [formGroup]="state.mainForm" autocomplete="off">
<mat-checkbox class="smtp-checkbox" [formControl]="state.mainForm.controls.tls" data-e2e="tls-checkbox">
{{ 'SETTING.SMTP.TLS' | translate }}
</mat-checkbox>
<cnsl-form-field class="smtp-form-field" label="Description">
<cnsl-label>{{ 'SETTING.SMTP.DESCRIPTION' | translate }}</cnsl-label>
<input
cnslInput
name="description"
[formControl]="state.mainForm.controls.description"
placeholder="description"
/>
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Host And Port">
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
<input
cnslInput
name="hostAndPort"
[formControl]="state.mainForm.controls.host"
placeholder="host:port"
required
/>
</cnsl-form-field>
<mat-checkbox class="smtp-checkbox" [formControl]="state.mainForm.controls.xoauth2">XOAUTH2</mat-checkbox>
<cnsl-form-field class="smtp-form-field" label="User">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input
id="smtp-user"
cnslInput
name="smtp-user"
autocomplete="smtp-user"
[formControl]="state.mainForm.controls.user"
required
/>
</cnsl-form-field>
@if ('tokenEndpoint' in state.authForm.controls) {
<cnsl-form-field class="smtp-form-field" label="tokenEndpoint">
<cnsl-label>Token Endpoint</cnsl-label>
<input cnslInput name="token-endpoint" [formControl]="state.authForm.controls.tokenEndpoint" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="scopes">
<cnsl-label>Scopes comma separated</cnsl-label>
<input cnslInput name="scopes" [formControl]="state.authForm.controls.scopes" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="clientId">
<cnsl-label>Client ID</cnsl-label>
<input cnslInput name="client-id" [formControl]="state.authForm.controls.clientId" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="clientSecret">
<cnsl-label>Client Secret</cnsl-label>
<input
cnslInput
type="password"
name="client-id"
[formControl]="state.authForm.controls.clientSecret"
required
/>
</cnsl-form-field>
} @else {
<cnsl-form-field class="smtp-form-field" label="Password">
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
<input
id="smtp-password"
cnslInput
name="smtp-password"
autocomplete="off webauthn"
[formControl]="state.authForm.controls.password"
[placeholder]="
'defaults' in state && state.defaults.auth.case === 'plain' ? state.defaults.auth.password.placeholder : ''
"
type="password"
/>
</cnsl-form-field>
}
</form>
<div class="smtp-create-actions">
<button
mat-raised-button
[disabled]="state.mainForm.invalid || state.authForm.invalid"
color="primary"
matStepperNext
data-e2e="continue-to-2nd-form"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</mat-step>
<mat-step [editable]="true">
<form [formGroup]="state.senderForm">
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SENDER_SETTINGS' | translate }}</ng-template>
<cnsl-form-field class="smtp-form-field" label="Sender Address">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input
cnslInput
name="senderAddress"
formControlName="senderAddress"
[placeholder]="state.senderEmailPlaceholder"
required
/>
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" placeholder="Zitadel" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
<input
cnslInput
name="senderReplyToAddress"
formControlName="replyToAddress"
placeholder="[email protected]"
/>
</cnsl-form-field>
</form>
<div class="smtp-create-actions">
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button
mat-raised-button
[disabled]="state.senderForm.invalid"
color="primary"
matStepperNext
data-e2e="continue-button"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</mat-step>
<mat-step [editable]="true" [completed]="'config' in state">
<form>
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SAVE_SETTINGS' | translate }}</ng-template>
<cnsl-info-section>
<div class="title-row">
<div class="left">
<h2 class="title">{{ 'SMTP.CREATE.STEPS.TEST.TITLE' | translate }}</h2>
</div>
<div class="right">
<button
color="primary"
mat-raised-button
class="continue-button"
(click)="testEmailConfigurationMutation.mutate()"
>
{{ 'ACTIONS.TEST' | translate }}
</button>
</div>
</div>
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.TEST.DESCRIPTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_EMAIL' | translate }}</cnsl-label>
<input
[disabled]="emailQuery.isLoading()"
cnslInput
(ngModelChange)="email.set($event)"
[ngModel]="email()"
[ngModelOptions]="{ standalone: true }"
data-e2e="email-test-dialog-input"
/>
</cnsl-form-field>
<div class="is-loading" *ngIf="testEmailConfigurationMutation.isPending()">
<mat-spinner diameter="50"></mat-spinner>
</div>
<cnsl-form-field
class="formfield"
*ngIf="testEmailConfigurationMutation.isError() || testEmailConfigurationMutation.isSuccess()"
>
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_RESULT' | translate }}</cnsl-label>
@let error = testEmailConfigurationMutation.error();
<textarea
cnslInput
[ngModel]="error ? error.message : ('SMTP.CREATE.STEPS.TEST.RESULT' | translate)"
[ngModelOptions]="{ standalone: true }"
></textarea>
</cnsl-form-field>
</cnsl-info-section>
<div class="smtp-create-actions">
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button
(click)="updateDataMutation.mutate()"
mat-raised-button
class="create-button"
color="primary"
data-e2e="create-button"
[disabled]="
state.mainForm.invalid || state.senderForm.invalid || (['iam.policy.write'] | hasRole | async) === false
"
>
{{ 'config' in state ? ('ACTIONS.SAVE' | translate) : ('ACTIONS.CREATE' | translate) }}
</button>
</div>
</form>
</mat-step>
<mat-step [editable]="true">
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.NEXT_STEPS' | translate }}</ng-template>
@if ('config' in state) {
<form>
@if (state.config.state === EmailProviderState.EMAIL_PROVIDER_ACTIVE) {
<cnsl-info-section>
<div class="title-row">
<div class="left">
<h2 class="title">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.TITLE' | translate }}</h2>
<div>
<a
mat-icon-button
card-actions
mat-icon-button
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
rel="noreferrer"
target="_blank"
>
<mat-icon class="next-icon">info_outline</mat-icon>
</a>
</div>
</div>
<div class="right">
<button
color="primary"
mat-raised-button
class="continue-button"
data-e2e="deactivate-button"
(click)="deactivateSMTPConfig(state.config.id); $event.stopPropagation()"
>
{{ 'ACTIONS.DEACTIVATE' | translate }}
</button>
</div>
</div>
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.DESCRIPTION' | translate }}</p>
</cnsl-info-section>
} @else if (state.config.state === EmailProviderState.EMAIL_PROVIDER_INACTIVE) {
<cnsl-info-section>
<div class="title-row">
<div class="left">
<h2 class="title">{{ 'SMTP.CREATE.STEPS.ACTIVATE.TITLE' | translate }}</h2>
<div>
<a
mat-icon-button
card-actions
mat-icon-button
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
rel="noreferrer"
target="_blank"
>
<mat-icon class="next-icon">info_outline</mat-icon>
</a>
</div>
</div>
<div class="right">
<button
color="primary"
mat-raised-button
class="continue-button"
data-e2e="activate-button"
(click)="activateSMTPConfig(state.config.id); $event.stopPropagation()"
>
{{ 'ACTIONS.ACTIVATE' | translate }}
</button>
</div>
</div>
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.ACTIVATE.DESCRIPTION' | translate }}</p>
</cnsl-info-section>
}
<div class="smtp-create-actions">
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button
mat-raised-button
class="create-button"
color="primary"
data-e2e="close-button"
(click)="location.back()"
>
{{ 'ACTIONS.CLOSE' | translate }}
</button>
</div>
</form>
}
</mat-step>
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
</mat-horizontal-stepper>
}
</cnsl-create-layout>