[Outlook integration] part 2 : GetMessages (#9612)
### Introducing - mock files in order to setup unit test on parsing outlook messages - special spec files for development purposes : dev.spec files. They are CI skipped with xdescribe but very useful for iterating on new messages format - main functionality : getMessages. We use microsoft default client to do so, using the $batch endpoint to group calls by 20 ### documentation final touch to add troubleshooting tips
This commit is contained in:
@ -17,6 +17,8 @@ export class EmailAliasManagerService {
|
|||||||
let handleAliases: string[];
|
let handleAliases: string[];
|
||||||
|
|
||||||
switch (connectedAccount.provider) {
|
switch (connectedAccount.provider) {
|
||||||
|
case 'microsoft':
|
||||||
|
return;
|
||||||
case 'google':
|
case 'google':
|
||||||
handleAliases =
|
handleAliases =
|
||||||
await this.googleEmailAliasManagerService.getHandleAliases(
|
await this.googleEmailAliasManagerService.getHandleAliases(
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export class RefreshAccessTokenService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const refreshToken = connectedAccount.refreshToken;
|
const refreshToken = connectedAccount.refreshToken;
|
||||||
|
let accessToken: string;
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new RefreshAccessTokenException(
|
throw new RefreshAccessTokenException(
|
||||||
@ -28,33 +29,39 @@ export class RefreshAccessTokenService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessToken: string;
|
switch (connectedAccount.provider) {
|
||||||
|
case 'microsoft':
|
||||||
|
return '';
|
||||||
|
case 'google': {
|
||||||
|
try {
|
||||||
|
accessToken = await this.refreshAccessToken(
|
||||||
|
connectedAccount,
|
||||||
|
refreshToken,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new RefreshAccessTokenException(
|
||||||
|
`Error refreshing access token for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.message}`,
|
||||||
|
RefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const connectedAccountRepository =
|
||||||
accessToken = await this.refreshAccessToken(
|
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||||
connectedAccount,
|
'connectedAccount',
|
||||||
refreshToken,
|
);
|
||||||
);
|
|
||||||
} catch (error) {
|
await connectedAccountRepository.update(
|
||||||
throw new RefreshAccessTokenException(
|
{ id: connectedAccount.id },
|
||||||
`Error refreshing access token for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.message}`,
|
{
|
||||||
RefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
|
accessToken,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Provider not supported for access token refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedAccountRepository =
|
|
||||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
|
||||||
'connectedAccount',
|
|
||||||
);
|
|
||||||
|
|
||||||
await connectedAccountRepository.update(
|
|
||||||
{ id: connectedAccount.id },
|
|
||||||
{
|
|
||||||
accessToken,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshAccessToken(
|
async refreshAccessToken(
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-accou
|
|||||||
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
|
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 { 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 { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
||||||
|
import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
|
||||||
|
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
|
||||||
|
import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
|
||||||
|
|
||||||
import { MicrosoftGetMessageListService } from './services/microsoft-get-message-list.service';
|
import { MicrosoftGetMessageListService } from './services/microsoft-get-message-list.service';
|
||||||
|
|
||||||
@ -23,8 +26,15 @@ import { MicrosoftGetMessageListService } from './services/microsoft-get-message
|
|||||||
providers: [
|
providers: [
|
||||||
MicrosoftClientProvider,
|
MicrosoftClientProvider,
|
||||||
MicrosoftGetMessageListService,
|
MicrosoftGetMessageListService,
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
MicrosoftFetchByBatchService,
|
||||||
|
MicrosoftHandleErrorService,
|
||||||
MicrosoftOAuth2ClientManagerService,
|
MicrosoftOAuth2ClientManagerService,
|
||||||
],
|
],
|
||||||
exports: [MicrosoftGetMessageListService, MicrosoftClientProvider],
|
exports: [
|
||||||
|
MicrosoftGetMessageListService,
|
||||||
|
MicrosoftClientProvider,
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class MessagingMicrosoftDriverModule {}
|
export class MessagingMicrosoftDriverModule {}
|
||||||
|
|||||||
@ -0,0 +1,241 @@
|
|||||||
|
export const microsoftGraphWithMessages = {
|
||||||
|
'@odata.context':
|
||||||
|
'https://graph.microsoft.com/beta/$metadata#Collection(message)',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
'@odata.type': '#microsoft.graph.message',
|
||||||
|
'@odata.etag': 'W/"CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAACItFa"',
|
||||||
|
id: 'AAMkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwBGAAAAAADzAhgkpMbwQYnkXH1D-Va3BwAadQ_1xAL8SLCZzf1KYyk_AAAAAAEMAAAadQ_1xAL8SLCZzf1KYyk_AAACJSmSAAA=',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@odata.nextLink':
|
||||||
|
"https://graph.microsoft.com/beta/me/mailFolders('inbox')/messages/delta?$skiptoken=jWnSM_TVmEdmKBzfVjDdNbDwpt3yYSUqEf9CFdhRcTxhbogC9oaTvY1ZdONMplHuz0pwtPay_qkEcFQ5RLEuDZ3O6IgnI5FXRcfekzOECWlL7zRVdGBidZ5TkXmXV7O7P8cxtvBMFJ2_dV951teFMatpdnD6hvksBK0Ff4tJKfo.HvZwAw_DM9PR3xf90ThtbqSdMCkGCHNPkjpaedxSBN3",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const microsoftGraphBatchWithTwoMessagesResponse = [
|
||||||
|
{
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'private',
|
||||||
|
'Content-Type':
|
||||||
|
'application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
'@odata.context':
|
||||||
|
"https://graph.microsoft.com/v1.0/$metadata#users('599cbaf7-873d-4b6f-b374-f87d3605e9e0')/messages/$entity",
|
||||||
|
'@odata.etag': 'W/"CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAACItFa"',
|
||||||
|
id: 'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
|
||||||
|
createdDateTime: '2025-01-10T13:31:37Z',
|
||||||
|
lastModifiedDateTime: '2025-01-10T13:31:38Z',
|
||||||
|
changeKey: 'CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAACItFa',
|
||||||
|
categories: [],
|
||||||
|
receivedDateTime: '2025-01-10T13:31:37Z',
|
||||||
|
sentDateTime: '2025-01-10T13:31:34Z',
|
||||||
|
hasAttachments: true,
|
||||||
|
internetMessageId:
|
||||||
|
'<FRZP194MB2383FF1CFE426952F85B1110981C3@FRAP194MB2383.EURP194.PROD.OUTLOOK.COM>',
|
||||||
|
subject: 'test email John: number 4',
|
||||||
|
bodyPreview: 'test 4',
|
||||||
|
importance: 'normal',
|
||||||
|
parentFolderId:
|
||||||
|
'AAMkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAuAAAAAADzAhgkpMbwQYnkXH1D-Va3AQAadQ_1xAL8SLCZzf1KYyk_AAAAAAEMAAA=',
|
||||||
|
conversationId:
|
||||||
|
'AAQkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAQAAZhOZ86nXZElRkxyGJRiY8=',
|
||||||
|
conversationIndex: 'AQHbY2PrBmE5nzqddkSVGTHIYlGJjw==',
|
||||||
|
isDeliveryReceiptRequested: null,
|
||||||
|
isReadReceiptRequested: false,
|
||||||
|
isRead: false,
|
||||||
|
isDraft: false,
|
||||||
|
webLink:
|
||||||
|
'https://outlook.office365.com/owa/?ItemID=AAkALgAAAAAAHYQDEapmEc2byACqAC%2FEWg0AGnUPtcQC%2FEiwmc39SmMpPgAAAiVYkAAA&exvsurl=1&viewmodel=ReadMessageItem',
|
||||||
|
inferenceClassification: 'focused',
|
||||||
|
body: {
|
||||||
|
contentType: 'text',
|
||||||
|
content: 'plain text format test 4',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'John l',
|
||||||
|
address: 'John.l@outlook.fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'John l',
|
||||||
|
address: 'John.l@outlook.fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toRecipients: [
|
||||||
|
{
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Walker',
|
||||||
|
address: 'walker@felixacme.onmicrosoft.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ccRecipients: [],
|
||||||
|
bccRecipients: [],
|
||||||
|
replyTo: [],
|
||||||
|
flag: {
|
||||||
|
flagStatus: 'notFlagged',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'private',
|
||||||
|
'Content-Type':
|
||||||
|
'application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
'@odata.context':
|
||||||
|
"https://graph.microsoft.com/v1.0/$metadata#users('599cbaf7-873d-4b6f-b374-f87d3605e9e0')/messages/$entity",
|
||||||
|
'@odata.etag': 'W/"CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAADw4Em"',
|
||||||
|
id: 'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAA8ZAfgAA',
|
||||||
|
createdDateTime: '2025-01-13T09:38:06Z',
|
||||||
|
lastModifiedDateTime: '2025-01-13T11:50:48Z',
|
||||||
|
changeKey: 'CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAADw4Em',
|
||||||
|
categories: [],
|
||||||
|
receivedDateTime: '2025-01-13T09:38:06Z',
|
||||||
|
sentDateTime: '2025-01-13T09:38:01Z',
|
||||||
|
hasAttachments: false,
|
||||||
|
internetMessageId:
|
||||||
|
'<dfe8ac36-cf4c-4842-a506-034548452966@az.westus2.microsoft.com>',
|
||||||
|
subject: 'test subject',
|
||||||
|
bodyPreview: 'You now have 2 licenses',
|
||||||
|
importance: 'normal',
|
||||||
|
parentFolderId:
|
||||||
|
'AAMkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAuAAAAAADzAhgkpMbwQYnkXH1D-Va3AQAadQ_1xAL8SLCZzf1KYyk_AAAAAAEMAAA=',
|
||||||
|
conversationId:
|
||||||
|
'AAQkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAQADz34qcnxpxEidnAJbZA-OI=',
|
||||||
|
conversationIndex: 'AQHbZZ7ZPPfipyfGnESJ2cAltkD84g==',
|
||||||
|
isDeliveryReceiptRequested: null,
|
||||||
|
isReadReceiptRequested: false,
|
||||||
|
isRead: false,
|
||||||
|
isDraft: false,
|
||||||
|
webLink:
|
||||||
|
'https://outlook.office365.com/owa/?ItemID=AAkALgAAAAAAHYQDEapmEc2byACqAC%2FEWg0AGnUPtcQC%2FEiwmc39SmMpPgAAA8ZAfgAA&exvsurl=1&viewmodel=ReadMessageItem',
|
||||||
|
inferenceClassification: 'focused',
|
||||||
|
body: {
|
||||||
|
contentType: 'text',
|
||||||
|
content: 'You will send a message in the plain text format',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Microsoft',
|
||||||
|
address: 'microsoft-noreply@microsoft.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Microsoft',
|
||||||
|
address: 'microsoft-noreply@microsoft.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toRecipients: [
|
||||||
|
{
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Walker',
|
||||||
|
address: 'walker@felixacme.onmicrosoft.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ccRecipients: [
|
||||||
|
{
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Antoine',
|
||||||
|
address: 'antoine@gmail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emailAddress: {
|
||||||
|
name: 'Cyril@acme2.com',
|
||||||
|
address: 'cyril@acme2.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bccRecipients: [],
|
||||||
|
replyTo: [],
|
||||||
|
flag: {
|
||||||
|
flagStatus: 'notFlagged',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const microsoftGraphBatchWithHtmlMessagesResponse = [
|
||||||
|
{
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'private',
|
||||||
|
'Content-Type':
|
||||||
|
'application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
'@odata.context':
|
||||||
|
"https://graph.microsoft.com/v1.0/$metadata#users('599cbaf7-873d-4b6f-b374-f87d3605e9e0')/messages/$entity",
|
||||||
|
'@odata.etag': 'W/"CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAACItFa"',
|
||||||
|
id: 'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
|
||||||
|
createdDateTime: '2025-01-10T13:31:37Z',
|
||||||
|
lastModifiedDateTime: '2025-01-10T13:31:38Z',
|
||||||
|
changeKey: 'CQAAABYAAAAadQ+1xAL8SLCZzf1KYyk+AAACItFa',
|
||||||
|
categories: [],
|
||||||
|
receivedDateTime: '2025-01-10T13:31:37Z',
|
||||||
|
sentDateTime: '2025-01-10T13:31:34Z',
|
||||||
|
hasAttachments: true,
|
||||||
|
internetMessageId:
|
||||||
|
'<FRZP194MB2383FF1CFE426952F85B1110981C3@FRAP194MB2383.EURP194.PROD.OUTLOOK.COM>',
|
||||||
|
subject: 'test email John: number 5',
|
||||||
|
bodyPreview: 'test 5',
|
||||||
|
importance: 'normal',
|
||||||
|
parentFolderId:
|
||||||
|
'AAMkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAuAAAAAADzAhgkpMbwQYnkXH1D-Va3AQAadQ_1xAL8SLCZzf1KYyk_AAAAAAEMAAA=',
|
||||||
|
conversationId:
|
||||||
|
'AAQkAGZlMDQ1NjU5LTUzN2UtNDAyMC1hNmVlLTZhZmExMGU3ZDU1NwAQAAZhOZ86nXZElRkxyGJRiY9=',
|
||||||
|
conversationIndex: 'AQHbY2PrBmE5nzqddkSVGTHIYlGJjr==',
|
||||||
|
isDeliveryReceiptRequested: null,
|
||||||
|
isReadReceiptRequested: false,
|
||||||
|
isRead: false,
|
||||||
|
isDraft: false,
|
||||||
|
webLink:
|
||||||
|
'https://outlook.office365.com/owa/?ItemID=AAkALgAAAAAAHYQDEapmEc2byACqAC%2FEWg0AGnUPtcQC%2FEiwmc39SmMpPgAAAiVYkAAA&exvsurl=1&viewmodel=ReadMessageItem',
|
||||||
|
inferenceClassification: 'focused',
|
||||||
|
body: {
|
||||||
|
contentType: 'html',
|
||||||
|
content:
|
||||||
|
'<html><head>\r\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body><div dir="ltr"><div dir="ltr"><div dir="ltr"><br></div></div><div id="divRplyFwdMsg" dir="ltr"><div> </div></div><div dir="ltr"><div class="x_elementToProof" style="font-family:Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">test 4</div></div></div></body></html>',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'John l',
|
||||||
|
address: 'John.l@outlook.fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
emailAddress: {
|
||||||
|
name: 'John l',
|
||||||
|
address: 'John.l@outlook.fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toRecipients: [],
|
||||||
|
ccRecipients: [],
|
||||||
|
bccRecipients: [],
|
||||||
|
replyTo: [],
|
||||||
|
flag: {
|
||||||
|
flagStatus: 'notFlagged',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
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 { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MicrosoftFetchByBatchService {
|
||||||
|
constructor(
|
||||||
|
private readonly microsoftClientProvider: MicrosoftClientProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async fetchAllByBatches(
|
||||||
|
messageIds: string[],
|
||||||
|
connectedAccount: Pick<
|
||||||
|
ConnectedAccountWorkspaceEntity,
|
||||||
|
'refreshToken' | 'id'
|
||||||
|
>,
|
||||||
|
): Promise<{
|
||||||
|
messageIdsByBatch: string[][];
|
||||||
|
batchResponses: MicrosoftGraphBatchResponse[];
|
||||||
|
}> {
|
||||||
|
const batchLimit = 20;
|
||||||
|
const batchResponses: MicrosoftGraphBatchResponse[] = [];
|
||||||
|
const messageIdsByBatch: string[][] = [];
|
||||||
|
|
||||||
|
const client =
|
||||||
|
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);
|
||||||
|
|
||||||
|
for (let i = 0; i < messageIds.length; i += batchLimit) {
|
||||||
|
const batchMessageIds = messageIds.slice(i, i + batchLimit);
|
||||||
|
|
||||||
|
messageIdsByBatch.push(batchMessageIds);
|
||||||
|
|
||||||
|
const batchRequests = batchMessageIds.map((messageId, index) => ({
|
||||||
|
id: (index + 1).toString(),
|
||||||
|
method: 'GET',
|
||||||
|
url: `/me/messages/${messageId}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Prefer: 'outlook.body-content-type="text"',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const batchResponse = await client
|
||||||
|
.api('/$batch')
|
||||||
|
.post({ requests: batchRequests });
|
||||||
|
|
||||||
|
batchResponses.push(batchResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageIdsByBatch,
|
||||||
|
batchResponses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||||
|
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
|
||||||
|
import { ConnectedAccountProvider } 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 { MicrosoftGetMessageListService } from './microsoft-get-message-list.service';
|
||||||
|
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
|
||||||
|
|
||||||
|
const refreshToken = 'replace-with-your-refresh-token';
|
||||||
|
|
||||||
|
xdescribe('Microsoft dev tests : get message list service', () => {
|
||||||
|
let service: MicrosoftGetMessageListService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [EnvironmentModule.forRoot({})],
|
||||||
|
providers: [
|
||||||
|
MicrosoftGetMessageListService,
|
||||||
|
MicrosoftClientProvider,
|
||||||
|
MicrosoftHandleErrorService,
|
||||||
|
MicrosoftOAuth2ClientManagerService,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<MicrosoftGetMessageListService>(
|
||||||
|
MicrosoftGetMessageListService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConnectedAccount = {
|
||||||
|
id: 'connected-account-id',
|
||||||
|
provider: ConnectedAccountProvider.MICROSOFT,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('Should fetch and return message list successfully', async () => {
|
||||||
|
const result = await service.getFullMessageList(mockConnectedAccount);
|
||||||
|
|
||||||
|
expect(result.messageExternalIds.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw token error', async () => {
|
||||||
|
const mockConnectedAccountUnvalid = {
|
||||||
|
id: 'connected-account-id',
|
||||||
|
provider: ConnectedAccountProvider.MICROSOFT,
|
||||||
|
refreshToken: 'invalid-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.getFullMessageList(mockConnectedAccountUnvalid),
|
||||||
|
).rejects.toThrowError('Access token is undefined or empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -35,7 +35,7 @@ export class MicrosoftGetMessageListService {
|
|||||||
.api(syncCursor || '/me/mailfolders/inbox/messages/delta?$select=id')
|
.api(syncCursor || '/me/mailfolders/inbox/messages/delta?$select=id')
|
||||||
.version('beta')
|
.version('beta')
|
||||||
.headers({
|
.headers({
|
||||||
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}`,
|
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
|
||||||
})
|
})
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
export interface MicrosoftGraphBatchResponse {
|
||||||
|
responses: {
|
||||||
|
id: string;
|
||||||
|
status: number;
|
||||||
|
headers?: {
|
||||||
|
'Cache-Control': string;
|
||||||
|
'Content-Type': string;
|
||||||
|
};
|
||||||
|
body?: {
|
||||||
|
'@odata.context'?: string;
|
||||||
|
'@odata.etag'?: string;
|
||||||
|
id?: string;
|
||||||
|
createdDateTime?: string;
|
||||||
|
lastModifiedDateTime?: string;
|
||||||
|
changeKey?: string;
|
||||||
|
categories?: any[];
|
||||||
|
receivedDateTime?: string;
|
||||||
|
sentDateTime?: string;
|
||||||
|
hasAttachments?: boolean;
|
||||||
|
internetMessageId?: string;
|
||||||
|
subject?: string;
|
||||||
|
bodyPreview?: string;
|
||||||
|
importance?: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
conversationId?: string;
|
||||||
|
conversationIndex?: string;
|
||||||
|
isDeliveryReceiptRequested?: boolean | null;
|
||||||
|
isReadReceiptRequested?: boolean;
|
||||||
|
isRead?: boolean;
|
||||||
|
isDraft?: boolean;
|
||||||
|
webLink?: string;
|
||||||
|
inferenceClassification?: string;
|
||||||
|
body?: {
|
||||||
|
contentType?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
sender?: {
|
||||||
|
emailAddress?: {
|
||||||
|
name?: string;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||||
|
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
|
||||||
|
import { ConnectedAccountProvider } 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 { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
|
||||||
|
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
|
||||||
|
|
||||||
|
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
|
||||||
|
|
||||||
|
const mockMessageIds = [
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAA8ZAfgAA',
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
|
||||||
|
];
|
||||||
|
|
||||||
|
const refreshToken = 'replace-with-your-refresh-token';
|
||||||
|
|
||||||
|
xdescribe('Microsoft dev tests : get messages service', () => {
|
||||||
|
let service: MicrosoftGetMessagesService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [EnvironmentModule.forRoot({})],
|
||||||
|
providers: [
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
MicrosoftHandleErrorService,
|
||||||
|
MicrosoftClientProvider,
|
||||||
|
MicrosoftOAuth2ClientManagerService,
|
||||||
|
MicrosoftFetchByBatchService,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<MicrosoftGetMessagesService>(
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConnectedAccount = {
|
||||||
|
id: 'connected-account-id',
|
||||||
|
provider: ConnectedAccountProvider.MICROSOFT,
|
||||||
|
handle: 'John.Walker@outlook.fr',
|
||||||
|
handleAliases: '',
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should fetch and format messages successfully', async () => {
|
||||||
|
const result = await service.getMessages(
|
||||||
|
mockMessageIds,
|
||||||
|
mockConnectedAccount,
|
||||||
|
'workspace-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||||
|
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
|
||||||
|
import { ConnectedAccountProvider } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import {
|
||||||
|
microsoftGraphBatchWithHtmlMessagesResponse,
|
||||||
|
microsoftGraphBatchWithTwoMessagesResponse,
|
||||||
|
} from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples';
|
||||||
|
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
||||||
|
import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
|
||||||
|
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
||||||
|
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
|
||||||
|
|
||||||
|
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
|
||||||
|
|
||||||
|
describe('Microsoft get messages service', () => {
|
||||||
|
let service: MicrosoftGetMessagesService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [EnvironmentModule.forRoot({})],
|
||||||
|
providers: [
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
MicrosoftHandleErrorService,
|
||||||
|
MicrosoftClientProvider,
|
||||||
|
MicrosoftOAuth2ClientManagerService,
|
||||||
|
MicrosoftFetchByBatchService,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<MicrosoftGetMessagesService>(
|
||||||
|
MicrosoftGetMessagesService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should format batch responses as messages', () => {
|
||||||
|
const batchResponses: MicrosoftGraphBatchResponse[] =
|
||||||
|
microsoftGraphBatchWithTwoMessagesResponse;
|
||||||
|
const connectedAccount = {
|
||||||
|
id: 'connected-account-id',
|
||||||
|
provider: ConnectedAccountProvider.MICROSOFT,
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
handle: 'John.l@outlook.fr',
|
||||||
|
handleAliases: '',
|
||||||
|
};
|
||||||
|
const messages = service.formatBatchResponsesAsMessages(
|
||||||
|
batchResponses,
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(messages).toHaveLength(2);
|
||||||
|
|
||||||
|
const responseExample1 =
|
||||||
|
microsoftGraphBatchWithTwoMessagesResponse[0].responses[0];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
messages.find(
|
||||||
|
(message) =>
|
||||||
|
message.externalId ===
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
|
||||||
|
),
|
||||||
|
).toStrictEqual({
|
||||||
|
externalId:
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
|
||||||
|
subject: responseExample1.body.subject,
|
||||||
|
receivedAt: new Date(responseExample1.body.receivedDateTime),
|
||||||
|
text: responseExample1.body.body.content,
|
||||||
|
headerMessageId: responseExample1.body.internetMessageId,
|
||||||
|
messageThreadExternalId: responseExample1.body.conversationId,
|
||||||
|
direction: 'OUTGOING',
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
displayName: 'John l',
|
||||||
|
handle: 'john.l@outlook.fr',
|
||||||
|
role: 'from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Walker',
|
||||||
|
handle: 'walker@felixacme.onmicrosoft.com',
|
||||||
|
role: 'to',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attachments: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseExample2 =
|
||||||
|
microsoftGraphBatchWithTwoMessagesResponse[0].responses[1];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
messages.filter(
|
||||||
|
(message) =>
|
||||||
|
message.externalId ===
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAA8ZAfgAA',
|
||||||
|
)[0],
|
||||||
|
).toStrictEqual({
|
||||||
|
externalId:
|
||||||
|
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAA8ZAfgAA',
|
||||||
|
subject: responseExample2.body.subject,
|
||||||
|
receivedAt: new Date(responseExample2.body.receivedDateTime),
|
||||||
|
text: responseExample2.body.body.content,
|
||||||
|
headerMessageId: responseExample2.body.internetMessageId,
|
||||||
|
messageThreadExternalId: responseExample2.body.conversationId,
|
||||||
|
direction: 'INCOMING',
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
displayName: 'Microsoft',
|
||||||
|
handle: 'microsoft-noreply@microsoft.com',
|
||||||
|
role: 'from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Walker',
|
||||||
|
handle: 'walker@felixacme.onmicrosoft.com',
|
||||||
|
role: 'to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Antoine',
|
||||||
|
handle: 'antoine@gmail.com',
|
||||||
|
role: 'cc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Cyril@acme2.com',
|
||||||
|
handle: 'cyril@acme2.com',
|
||||||
|
role: 'cc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attachments: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set empty text for html responses', () => {
|
||||||
|
const batchResponses: MicrosoftGraphBatchResponse[] =
|
||||||
|
microsoftGraphBatchWithHtmlMessagesResponse;
|
||||||
|
const connectedAccount = {
|
||||||
|
id: 'connected-account-id',
|
||||||
|
provider: ConnectedAccountProvider.MICROSOFT,
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
handle: 'John.l@outlook.fr',
|
||||||
|
handleAliases: '',
|
||||||
|
};
|
||||||
|
const messages = service.formatBatchResponsesAsMessages(
|
||||||
|
batchResponses,
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseExample =
|
||||||
|
microsoftGraphBatchWithHtmlMessagesResponse[0].responses[0];
|
||||||
|
|
||||||
|
expect(messages[0]).toStrictEqual({
|
||||||
|
externalId: responseExample.body.id,
|
||||||
|
subject: responseExample.body.subject,
|
||||||
|
receivedAt: new Date(responseExample.body.receivedDateTime),
|
||||||
|
text: '',
|
||||||
|
headerMessageId: responseExample.body.internetMessageId,
|
||||||
|
messageThreadExternalId: responseExample.body.conversationId,
|
||||||
|
direction: 'OUTGOING',
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
displayName: responseExample.body.sender.emailAddress.name,
|
||||||
|
handle:
|
||||||
|
responseExample.body.sender.emailAddress.address.toLowerCase(),
|
||||||
|
role: 'from',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attachments: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
|
||||||
|
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
||||||
|
import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
||||||
|
import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
|
import { MicrosoftFetchByBatchService } from './microsoft-fetch-by-batch.service';
|
||||||
|
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
|
||||||
|
|
||||||
|
type ConnectedAccountType = Pick<
|
||||||
|
ConnectedAccountWorkspaceEntity,
|
||||||
|
'refreshToken' | 'id' | 'provider' | 'handle' | 'handleAliases'
|
||||||
|
>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MicrosoftGetMessagesService {
|
||||||
|
private readonly logger = new Logger(MicrosoftGetMessagesService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly microsoftFetchByBatchService: MicrosoftFetchByBatchService,
|
||||||
|
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getMessages(
|
||||||
|
messageIds: string[],
|
||||||
|
connectedAccount: ConnectedAccountType,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<MessageWithParticipants[]> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { batchResponses } =
|
||||||
|
await this.microsoftFetchByBatchService.fetchAllByBatches(
|
||||||
|
messageIds,
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = this.formatBatchResponsesAsMessages(
|
||||||
|
batchResponses,
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Messaging import for workspace ${workspaceId} and account ${
|
||||||
|
connectedAccount.id
|
||||||
|
} fetched ${messages.length} messages in ${endTime - startTime}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
} catch (error) {
|
||||||
|
this.microsoftHandleErrorService.handleMicrosoftMessageListFetchError(
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public formatBatchResponsesAsMessages(
|
||||||
|
batchResponses: MicrosoftGraphBatchResponse[],
|
||||||
|
connectedAccount: ConnectedAccountType,
|
||||||
|
): MessageWithParticipants[] {
|
||||||
|
return batchResponses.flatMap((batchResponse) => {
|
||||||
|
return this.formatBatchResponseAsMessages(
|
||||||
|
batchResponse,
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBatchResponseAsMessages(
|
||||||
|
batchResponse: MicrosoftGraphBatchResponse,
|
||||||
|
connectedAccount: ConnectedAccountType,
|
||||||
|
): MessageWithParticipants[] {
|
||||||
|
const parsedResponses = this.parseBatchResponse(batchResponse);
|
||||||
|
|
||||||
|
const messages = parsedResponses.map((response) => {
|
||||||
|
if ('error' in response) {
|
||||||
|
this.microsoftHandleErrorService.handleMicrosoftMessageListFetchError(
|
||||||
|
response.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const participants = [
|
||||||
|
...formatAddressObjectAsParticipants(
|
||||||
|
response?.from?.emailAddress,
|
||||||
|
'from',
|
||||||
|
),
|
||||||
|
...formatAddressObjectAsParticipants(
|
||||||
|
response?.toRecipients?.map((recipient) => recipient.emailAddress),
|
||||||
|
'to',
|
||||||
|
),
|
||||||
|
...formatAddressObjectAsParticipants(
|
||||||
|
response?.ccRecipients?.map((recipient) => recipient.emailAddress),
|
||||||
|
'cc',
|
||||||
|
),
|
||||||
|
...formatAddressObjectAsParticipants(
|
||||||
|
response?.bccRecipients?.map((recipient) => recipient.emailAddress),
|
||||||
|
'bcc',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
externalId: response.id,
|
||||||
|
subject: response.subject || '',
|
||||||
|
receivedAt: new Date(response.receivedDateTime),
|
||||||
|
text:
|
||||||
|
response.body?.contentType === 'text' ? response.body?.content : '',
|
||||||
|
headerMessageId: response.internetMessageId,
|
||||||
|
messageThreadExternalId: response.conversationId,
|
||||||
|
direction: computeMessageDirection(
|
||||||
|
response.from.emailAddress.address,
|
||||||
|
connectedAccount,
|
||||||
|
),
|
||||||
|
participants: participants,
|
||||||
|
attachments: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return messages.filter(isDefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseBatchResponse(batchResponse: MicrosoftGraphBatchResponse) {
|
||||||
|
if (!batchResponse?.responses) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return batchResponse.responses.map((response: any) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: response.error };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { GraphError } from '@microsoft/microsoft-graph-client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageImportDriverException,
|
||||||
|
MessageImportDriverExceptionCode,
|
||||||
|
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MicrosoftHandleErrorService {
|
||||||
|
public handleMicrosoftMessageListFetchError(error: GraphError): void {
|
||||||
|
if (error.statusCode === 401) {
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
'Unauthorized access to Microsoft Graph API',
|
||||||
|
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 403) {
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
'Forbidden access to Microsoft Graph API',
|
||||||
|
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
`Microsoft Graph API error: ${error.message}`,
|
||||||
|
MessageImportDriverExceptionCode.UNKNOWN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service';
|
import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service';
|
||||||
|
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
|
||||||
import {
|
import {
|
||||||
MessageImportException,
|
MessageImportException,
|
||||||
MessageImportExceptionCode,
|
MessageImportExceptionCode,
|
||||||
@ -14,6 +15,7 @@ export type GetMessagesResponse = MessageWithParticipants[];
|
|||||||
export class MessagingGetMessagesService {
|
export class MessagingGetMessagesService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly gmailGetMessagesService: GmailGetMessagesService,
|
private readonly gmailGetMessagesService: GmailGetMessagesService,
|
||||||
|
private readonly microsoftGetMessagesService: MicrosoftGetMessagesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getMessages(
|
public async getMessages(
|
||||||
@ -36,6 +38,12 @@ export class MessagingGetMessagesService {
|
|||||||
connectedAccount,
|
connectedAccount,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
case 'microsoft':
|
||||||
|
return this.microsoftGetMessagesService.getMessages(
|
||||||
|
messageIds,
|
||||||
|
connectedAccount,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new MessageImportException(
|
throw new MessageImportException(
|
||||||
`Provider ${connectedAccount.provider} is not supported`,
|
`Provider ${connectedAccount.provider} is not supported`,
|
||||||
|
|||||||
@ -61,6 +61,10 @@ yarn command:prod cron:calendar:ongoing-stale
|
|||||||
|
|
||||||
## For Outlook and Outlook Calendar (Microsoft 365)
|
## For Outlook and Outlook Calendar (Microsoft 365)
|
||||||
|
|
||||||
|
<ArticleWarning>
|
||||||
|
Users must have a [Microsoft 365 Licence](https://admin.microsoft.com/Adminportal/Home) to be able to use the Calendar and Messaging API. They will not be able to sync their account on Twenty without one.
|
||||||
|
</ArticleWarning>
|
||||||
|
|
||||||
### Create a project in Microsoft Azure
|
### 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.
|
You will need to create a project in [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) and get the credentials.
|
||||||
|
|||||||
@ -70,6 +70,10 @@ Most of the time, it's because the `worker` is not running in the background. Tr
|
|||||||
npx nx worker twenty-server
|
npx nx worker twenty-server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Cannot connect my Microsoft 365 account
|
||||||
|
|
||||||
|
Most of the time, it's because your admin has not enabled the Microsoft 365 Licence for your account. Check [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
|
||||||
|
|
||||||
#### While running `yarn` warnings appear in console
|
#### While running `yarn` warnings appear in console
|
||||||
|
|
||||||
Warnings are informing about pulling additional dependencies which aren't explicitly stated in `package.json`, so as long as no breaking error appears, everything should work as expected.
|
Warnings are informing about pulling additional dependencies which aren't explicitly stated in `package.json`, so as long as no breaking error appears, everything should work as expected.
|
||||||
|
|||||||
Reference in New Issue
Block a user