Skip to content

Commit

Permalink
NAS-132807: OAuth support for Microsoft Outlook Mail (#11206)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKarpov98 authored Dec 20, 2024
1 parent 4b3fe41 commit a7794a4
Show file tree
Hide file tree
Showing 99 changed files with 856 additions and 220 deletions.
4 changes: 4 additions & 0 deletions src/app/helptext/system/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const helptextSystemEmail = {
placeholder: 'GMail OAuth',
tooltip: T('Enable GMail OAuth authentication.'),
},
outlook: {
placeholder: 'Outlook OAuth',
tooltip: T('Enable Outlook OAuth authentication.'),
},
},

auth: {
Expand Down
13 changes: 10 additions & 3 deletions src/app/interfaces/mail-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface MailConfig {
fromemail: string;
fromname: string;
id: number;
oauth: GmailOauthConfig | Record<string, never>;
oauth: MailOauthConfig | Record<string, never>;
outgoingserver: string;
pass: string;
port: number;
Expand All @@ -13,18 +13,19 @@ export interface MailConfig {
user: string;
}

export interface GmailOauthConfig {
export interface MailOauthConfig {
client_id: string;
client_secret: string;
refresh_token: string;
provider: MailSendMethod;
}

export interface MailConfigUpdate {
// Smtp field is actually about smtp authentication.
smtp?: boolean;
fromemail: string;
fromname: string;
oauth: GmailOauthConfig;
oauth: MailOauthConfig;
outgoingserver?: string;
pass?: string;
port?: number;
Expand All @@ -45,3 +46,9 @@ export interface SendMailParams {
queue?: boolean;
extra_headers?: Record<string, unknown>;
}

export enum MailSendMethod {
Smtp = 'smtp',
Gmail = 'gmail',
Outlook = 'outlook',
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum OauthButtonType {
Jira = 'Jira',
Provider = 'Provider',
Gmail = 'Gmail',
Outlook = 'Outlook',
}
58 changes: 46 additions & 12 deletions src/app/modules/buttons/oauth-button/oauth-button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
import { MatButton } from '@angular/material/button';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { WINDOW } from 'app/helpers/window.helper';
import { GmailOauthConfig } from 'app/interfaces/mail-config.interface';
import { MailSendMethod, MailOauthConfig } from 'app/interfaces/mail-config.interface';
import { OauthMessage } from 'app/interfaces/oauth-message.interface';
import { OauthButtonType } from 'app/modules/buttons/oauth-button/interfaces/oauth-button.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
Expand Down Expand Up @@ -35,10 +35,14 @@ export class OauthButtonComponent implements OnDestroy {
readonly loggedIn = output<unknown>();

private readonly jiraAuthFn = (message: OauthJiraMessage): void => this.onLogInWithJiraSuccess(message);
private readonly gmailAuthFn = (message: OauthMessage<GmailOauthConfig>): void => {
private readonly gmailAuthFn = (message: OauthMessage<MailOauthConfig>): void => {
this.onLogInWithGmailSuccess(message);
};

private readonly outlookAuthFn = (message: OauthMessage<MailOauthConfig>): void => {
this.onLogInWithOutlookSuccess(message);
};

protected buttonText = computed(() => {
switch (this.oauthType()) {
case OauthButtonType.Jira:
Expand All @@ -58,6 +62,12 @@ export class OauthButtonComponent implements OnDestroy {
return this.translate.instant('Logged In To Gmail');
}
return this.translate.instant('Log In To Gmail');

case OauthButtonType.Outlook:
if (this.isLoggedIn()) {
return this.translate.instant('Logged In To Outlook');
}
return this.translate.instant('Log In To Outlook');
}

return '';
Expand Down Expand Up @@ -86,13 +96,29 @@ export class OauthButtonComponent implements OnDestroy {
case OauthButtonType.Gmail:
this.onLoginWithGmail();
break;
case OauthButtonType.Outlook:
this.onLoginWithOutlook();
break;
}
}

private onLoginWithJira(): void {
this.doCommonOauthLoginLogic(this.jiraAuthFn);
}

private onLogInWithProvider(): void {
const authFn = (message: OauthMessage<OauthProviderData>): void => this.onLoggedInWithProviderSuccess(message);
this.doCommonOauthLoginLogic(authFn);
}

private onLoginWithGmail(): void {
this.doCommonOauthLoginLogic(this.gmailAuthFn);
}

private onLoginWithOutlook(): void {
this.doCommonOauthLoginLogic(this.outlookAuthFn);
}

private onLogInWithJiraSuccess(message: OauthJiraMessage): void {
const token = message.data as string;
if (typeof token !== 'string') {
Expand All @@ -102,24 +128,32 @@ export class OauthButtonComponent implements OnDestroy {
this.cdr.markForCheck();
}

private onLoginWithGmail(): void {
this.doCommonOauthLoginLogic(this.gmailAuthFn);
}

private onLogInWithGmailSuccess(message: OauthMessage<GmailOauthConfig>): void {
private onLogInWithGmailSuccess(message: OauthMessage<MailOauthConfig>): void {
if (message.data.oauth_portal) {
if (message.data.error) {
this.handleProviderError(message.data.error);
} else {
this.loggedIn.emit(message.data.result);
this.loggedIn.emit({
...message.data.result,
provider: MailSendMethod.Gmail,
});
this.cdr.markForCheck();
}
}
}

private onLogInWithProvider(): void {
const authFn = (message: OauthMessage<OauthProviderData>): void => this.onLoggedInWithProviderSuccess(message);
this.doCommonOauthLoginLogic(authFn);
private onLogInWithOutlookSuccess(message: OauthMessage<MailOauthConfig>): void {
if (message.data.oauth_portal) {
if (message.data.error) {
this.handleProviderError(message.data.error);
} else {
this.loggedIn.emit({
...message.data.result,
provider: MailSendMethod.Outlook,
});
this.cdr.markForCheck();
}
}
}

private onLoggedInWithProviderSuccess = (message: OauthMessage<OauthProviderData>): void => {
Expand All @@ -140,7 +174,7 @@ export class OauthButtonComponent implements OnDestroy {

private doCommonOauthLoginLogic(
authFn: (
message: OauthMessage<GmailOauthConfig> | OauthMessage<OauthProviderData> | OauthJiraMessage
message: OauthMessage<MailOauthConfig> | OauthMessage<OauthProviderData> | OauthJiraMessage
) => void,
): void {
this.window.removeEventListener('message', authFn, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ <h3>{{ 'Email' | translate }}</h3>
<mat-list-item [ixUiSearch]="searchableElements.elements.sendMethod">
<span class="label">{{ 'Send Mail Method' | translate }}:</span>
<span *ixWithLoadingState="emailConfig$ as emailConfig" class="value">
{{ !emailConfig?.oauth?.client_id ? helptext.send_mail_method.smtp.placeholder : helptext.send_mail_method.gmail.placeholder }}
{{
!emailConfig?.oauth?.client_id
? helptext.send_mail_method.smtp.placeholder
: helptext.send_mail_method[emailConfig?.oauth?.provider]?.placeholder
}}
</span>
</mat-list-item>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectat
import { of } from 'rxjs';
import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { MailSecurity } from 'app/enums/mail-security.enum';
import { MailConfig } from 'app/interfaces/mail-config.interface';
import { MailConfig, MailOauthConfig } from 'app/interfaces/mail-config.interface';
import { EmailCardComponent } from 'app/pages/system/general-settings/email/email-card/email-card.component';
import { EmailFormComponent } from 'app/pages/system/general-settings/email/email-form/email-form.component';
import { SlideInService } from 'app/services/slide-in.service';
Expand All @@ -24,7 +24,7 @@ const fakeEmailConfig: MailConfig = {
user: null as string,
};

describe('EmailCardComponent', () => {
describe('EmailCardComponent with SMTP', () => {
let spectator: Spectator<EmailCardComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
Expand Down Expand Up @@ -61,3 +61,69 @@ describe('EmailCardComponent', () => {
expect(spectator.inject(SlideInService).open).toHaveBeenCalledWith(EmailFormComponent, { data: fakeEmailConfig });
});
});

describe('EmailCardComponent with Gmail OAuth', () => {
let spectator: Spectator<EmailCardComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: EmailCardComponent,
providers: [
mockApi([
mockCall('mail.config', {
...fakeEmailConfig,
oauth: { client_id: '123', provider: 'gmail' } as MailOauthConfig,
}),
]),
mockProvider(SlideInService, {
open: jest.fn(() => ({ slideInClosed$: of() })),
}),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows Email related settings', async () => {
const items = await loader.getAllHarnesses(MatListItemHarness);
const itemTexts = await parallel(() => items.map((item) => item.getFullText()));

expect(itemTexts).toEqual([
'Send Mail Method: GMail OAuth',
]);
});
});

describe('EmailCardComponent with Outlook OAuth', () => {
let spectator: Spectator<EmailCardComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: EmailCardComponent,
providers: [
mockApi([
mockCall('mail.config', {
...fakeEmailConfig,
oauth: { client_id: '123', provider: 'outlook' } as MailOauthConfig,
}),
]),
mockProvider(SlideInService, {
open: jest.fn(() => ({ slideInClosed$: of() })),
}),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows Email related settings', async () => {
const items = await loader.getAllHarnesses(MatListItemHarness);
const itemTexts = await parallel(() => items.map((item) => item.getFullText()));

expect(itemTexts).toEqual([
'Send Mail Method: Outlook OAuth',
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const emailCardElements = {
},
sendMethod: {
hierarchy: [T('Send Method')],
synonyms: [T('SMTP'), T('Gmail')],
synonyms: [T('SMTP'), T('Gmail'), T('Outlook')],
},
},
} satisfies UiSearchableElement;
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,16 @@
<div class="oauth-message">
@if (hasOauthAuthorization) {
<ix-icon name="check_circle"></ix-icon>
{{ 'Gmail credentials have been applied.' | translate }}
{{ '{oauthType} credentials have been applied.' | translate: { oauthType } }}
} @else {
<ix-icon name="info"></ix-icon>
{{ 'Log in to Gmail to set up Oauth credentials.' | translate }}
{{ 'Log in to {oauthType} to set up Oauth credentials.' | translate: { oauthType } }}
}
</div>
<ix-oauth-button
testId="login-to-gmail"
[oauthType]="oauthType.Gmail"
[oauthUrl]="'https://truenas.com/oauth/gmail?origin='"
testId="login-to-oauth-provider"
[oauthType]="oauthType"
[oauthUrl]="oauthUrl"
[isLoggedIn]="hasOauthAuthorization"
(loggedIn)="onLoggedIn($event)"
></ix-oauth-button>
Expand All @@ -107,6 +107,7 @@
mat-button
type="button"
ixTest="send-test-mail"
[disabled]="!isValid || isLoading"
(click)="onSendTestEmailPressed()"
>
{{ 'Send Test Mail' | translate }}
Expand Down
Loading

0 comments on commit a7794a4

Please sign in to comment.