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

[Microsoft integration] getFullMessageList #9544

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
import { CALENDAR_EVENTS_IMPORT_CRON_PATTERN } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';

const CALENDAR_EVENTS_LIST_CRON_PATTERN = '*/5 * * * *';

@Command({
name: 'cron:calendar:calendar-event-list-fetch',
Expand All @@ -23,7 +24,9 @@ export class CalendarEventListFetchCronCommand extends CommandRunner {
CalendarEventListFetchCronJob.name,
undefined,
{
repeat: { pattern: CALENDAR_EVENTS_IMPORT_CRON_PATTERN },
repeat: {
pattern: CALENDAR_EVENTS_LIST_CRON_PATTERN,
},
},
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';

import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';

import { MicrosoftGetMessageListService } from './services/microsoft-get-message-list.service';

@Module({
imports: [
EnvironmentModule,
MessagingCommonModule,
FeatureFlagModule,
OAuth2ClientManagerModule,
WorkspaceDataSourceModule,
ObjectMetadataRepositoryModule,
],
providers: [
MicrosoftClientProvider,
MicrosoftGetMessageListService,
MicrosoftOAuth2ClientManagerService,
],
exports: [MicrosoftGetMessageListService, MicrosoftClientProvider],
})
export class MessagingMicrosoftDriverModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';

import { Client } from '@microsoft/microsoft-graph-client';

import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';

@Injectable()
export class MicrosoftClientProvider {
constructor(
private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService,
) {}

public async getMicrosoftClient(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'refreshToken' | 'id'
>,
): Promise<Client> {
try {
return await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.refreshToken,
);
} catch (error) {
throw new Error(
`Failed to get Microsoft client: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Injectable } from '@nestjs/common';

import {
PageCollection,
PageIterator,
PageIteratorCallback,
} from '@microsoft/microsoft-graph-client';

import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { GetFullMessageListResponse } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service';

// Microsoft API limit is 1000 messages per request on this endpoint
const MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT = 1000;

@Injectable()
export class MicrosoftGetMessageListService {
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
) {}

public async getFullMessageList(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id'
>,
syncCursor?: string,
): Promise<GetFullMessageListResponse> {
const messageExternalIds: string[] = [];

const microsoftClient =
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);

const response: PageCollection = await microsoftClient
.api(syncCursor || '/me/mailfolders/inbox/messages/delta?$select=id')
.version('beta')
guillim marked this conversation as resolved.
Show resolved Hide resolved
.headers({
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}`,
})
.get();
Comment on lines +34 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No try-catch block around API call that could fail due to network issues or invalid tokens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only interesting thing we might talk out loud


const callback: PageIteratorCallback = (data) => {
messageExternalIds.push(data.id);

return true;
};

const pageIterator = new PageIterator(microsoftClient, response, callback);

await pageIterator.iterate();

return {
messageExternalIds: messageExternalIds,
nextSyncCursor: pageIterator.getDeltaLink() || '',
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MessagingMessageListFetchCronJob } from 'src/modules/messaging/message-
import { MessagingMessagesImportCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job';
import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module';
import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job';
import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache';
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
Expand All @@ -40,6 +41,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
RefreshAccessTokenManagerModule,
WorkspaceDataSourceModule,
MessagingGmailDriverModule,
MessagingMicrosoftDriverModule,
MessagingCommonModule,
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';

import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
import { MicrosoftGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service';
import {
MessageImportException,
MessageImportExceptionCode,
Expand All @@ -22,6 +23,7 @@ export type GetPartialMessageListResponse = {
export class MessagingGetMessageListService {
constructor(
private readonly gmailGetMessageListService: GmailGetMessageListService,
private readonly microsoftGetMessageListService: MicrosoftGetMessageListService,
) {}

public async getFullMessageList(
Expand All @@ -36,11 +38,9 @@ export class MessagingGetMessageListService {
connectedAccount,
);
case 'microsoft':
// TODO: Placeholder
return {
messageExternalIds: [],
nextSyncCursor: '',
};
return this.microsoftGetMessageListService.getFullMessageList(
connectedAccount,
);
default:
throw new MessageImportException(
`Provider ${connectedAccount.provider} is not supported`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,58 @@ yarn command:prod cron:messaging:ongoing-stale
yarn command:prod cron:calendar:ongoing-stale
```

## For Outlook and Outlook Calendar (Microsoft 365)

### Create a project in Microsoft Azure

You will need to create a project in [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) and get the credentials.

Then you can set the following environment variables:

- `AUTH_MICROSOFT_ENABLED=true`
- `AUTH_MICROSOFT_CLIENT_ID=<client-id>`
- `AUTH_MICROSOFT_TENANT_ID=<tenant-id>`
- `AUTH_MICROSOFT_CLIENT_SECRET=<client-secret>`
- `AUTH_MICROSOFT_CALLBACK_URL=https://<your-domain>/auth/microsoft/redirect` if you want to use Microsoft SSO
- `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://<your-domain>/auth/microsoft-apis/get-access-token`

### Enable APIs

On Microsoft Azure Console enable the following APIs in "Permissions":

- Microsoft Graph: Mail.Read
- Microsoft Graph: Calendars.Read
- Microsoft Graph: User.Read.All
- Microsoft Graph: openid
- Microsoft Graph: email
- Microsoft Graph: profile
- Microsoft Graph: offline_access

### Authorized redirect URIs

You need to add the following redirect URIs to your project:
- `https://<your-domain>/auth/microsoft/redirect` if you want to use Microsoft SSO
- `https://<your-domain>/auth/microsoft-apis/get-access-token`

### If your app is in test mode

If your app is in test mode, you will need to add test users to your project.

Add your test users to the "Users and groups" section.

### Start the cron jobs

Register the following recurring jobs:
```
# from your worker container
yarn command:prod cron:messaging:messages-import
yarn command:prod cron:messaging:message-list-fetch
yarn command:prod cron:calendar:calendar-event-list-fetch
yarn command:prod cron:calendar:calendar-event-import
yarn command:prod cron:messaging:ongoing-stale
yarn command:prod cron:calendar:ongoing-stale
```

# Setup Environment Variables

## Frontend
Expand Down
Loading