mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-11 12:57:59 +00:00
UBERF-10925: Save gmail messages only for integration owner (#9061)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
parent
2ac02e6cc7
commit
14c7541bd3
@ -35,7 +35,7 @@ export function createMessageManager (
|
|||||||
socialId: SocialId
|
socialId: SocialId
|
||||||
): IMessageManager {
|
): IMessageManager {
|
||||||
if (config.Version === 'v2') {
|
if (config.Version === 'v2') {
|
||||||
return new MessageManagerV2(ctx, attachmentHandler, client, keyValueClient, accountClient, token)
|
return new MessageManagerV2(ctx, attachmentHandler, client, keyValueClient, accountClient, token, socialId._id)
|
||||||
} else {
|
} else {
|
||||||
return new MessageManagerV1(ctx, client, attachmentHandler, socialId._id, workspace)
|
return new MessageManagerV1(ctx, client, attachmentHandler, socialId._id, workspace)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import { type GaxiosResponse } from 'gaxios'
|
|||||||
import { gmail_v1 } from 'googleapis'
|
import { gmail_v1 } from 'googleapis'
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
import { TxOperations, type MeasureContext } from '@hcengineering/core'
|
import { type MeasureContext, PersonId, TxOperations } from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
createMessages,
|
createMessages,
|
||||||
parseEmailHeader,
|
parseEmailHeader,
|
||||||
@ -40,7 +40,8 @@ export class MessageManagerV2 implements IMessageManager {
|
|||||||
private readonly txClient: TxOperations,
|
private readonly txClient: TxOperations,
|
||||||
private readonly keyValueClient: KeyValueClient,
|
private readonly keyValueClient: KeyValueClient,
|
||||||
private readonly accountClient: AccountClient,
|
private readonly accountClient: AccountClient,
|
||||||
private readonly token: string
|
private readonly token: string,
|
||||||
|
private readonly personId: PersonId
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async saveMessage (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string): Promise<void> {
|
async saveMessage (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string): Promise<void> {
|
||||||
@ -65,7 +66,8 @@ export class MessageManagerV2 implements IMessageManager {
|
|||||||
this.token,
|
this.token,
|
||||||
this.wsInfo,
|
this.wsInfo,
|
||||||
res,
|
res,
|
||||||
attachments
|
attachments,
|
||||||
|
[this.personId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
401
services/mail/mail-common/src/__tests__/message.test.ts
Normal file
401
services/mail/mail-common/src/__tests__/message.test.ts
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
//
|
||||||
|
// Copyright © 2025 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { createMessages } from '../message'
|
||||||
|
import { WorkspaceLoginInfo } from '@hcengineering/account-client'
|
||||||
|
import { PersonSpace } from '@hcengineering/contact'
|
||||||
|
import { MeasureContext, PersonId, PersonUuid, Ref, TxOperations } from '@hcengineering/core'
|
||||||
|
import { KeyValueClient } from '@hcengineering/kvs-client'
|
||||||
|
import { Producer } from 'kafkajs'
|
||||||
|
import { Attachment, BaseConfig, EmailContact, EmailMessage } from '../types'
|
||||||
|
import { PersonCacheFactory } from '../person'
|
||||||
|
import { PersonSpacesCacheFactory } from '../personSpaces'
|
||||||
|
import { ChannelCacheFactory } from '../channel'
|
||||||
|
import { ThreadLookupService } from '../thread'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('../person')
|
||||||
|
jest.mock('../personSpaces')
|
||||||
|
jest.mock('../channel')
|
||||||
|
jest.mock('../thread')
|
||||||
|
jest.mock('kafkajs')
|
||||||
|
|
||||||
|
describe('createMessages', () => {
|
||||||
|
// Setup test vars
|
||||||
|
let mockConfig: BaseConfig
|
||||||
|
let mockCtx: MeasureContext
|
||||||
|
let mockTxClient: jest.Mocked<TxOperations>
|
||||||
|
let mockKvsClient: jest.Mocked<KeyValueClient>
|
||||||
|
let mockProducer: jest.Mocked<Producer>
|
||||||
|
let mockToken: string
|
||||||
|
let mockWsInfo: WorkspaceLoginInfo
|
||||||
|
let mockMessage: EmailMessage
|
||||||
|
let mockAttachments: Attachment[]
|
||||||
|
|
||||||
|
// Mock person objects
|
||||||
|
const mockFromPerson = {
|
||||||
|
socialId: 'from-person-id' as PersonId,
|
||||||
|
uuid: 'from-uuid' as PersonUuid,
|
||||||
|
localPerson: 'from-local'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockToPerson1 = {
|
||||||
|
socialId: 'to-person-id-1' as PersonId,
|
||||||
|
uuid: 'to-uuid-1' as PersonUuid,
|
||||||
|
localPerson: 'to-local-1'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockToPerson2 = {
|
||||||
|
socialId: 'to-person-id-2' as PersonId,
|
||||||
|
uuid: 'to-uuid-2' as PersonUuid,
|
||||||
|
localPerson: 'to-local-2'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock email contacts
|
||||||
|
const mockFromContact: EmailContact = {
|
||||||
|
email: 'from@example.com',
|
||||||
|
firstName: 'From',
|
||||||
|
lastName: 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockToContact1: EmailContact = {
|
||||||
|
email: 'to1@example.com',
|
||||||
|
firstName: 'To1',
|
||||||
|
lastName: 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockToContact2: EmailContact = {
|
||||||
|
email: 'to2@example.com',
|
||||||
|
firstName: 'To2',
|
||||||
|
lastName: 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock PersonCache class
|
||||||
|
const mockPersonCache = {
|
||||||
|
ensurePerson: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock PersonSpaces class
|
||||||
|
const mockPersonSpacesCache = {
|
||||||
|
getPersonSpaces: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock ChannelCache class
|
||||||
|
const mockChannelCache = {
|
||||||
|
getOrCreateChannel: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock ThreadLookupService class
|
||||||
|
const mockThreadLookup = {
|
||||||
|
getThreadId: jest.fn(),
|
||||||
|
getParentThreadId: jest.fn(),
|
||||||
|
setThreadId: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock space
|
||||||
|
const mockSpace: PersonSpace = {
|
||||||
|
_id: 'space-id' as Ref<PersonSpace>,
|
||||||
|
_class: 'class.contact.PersonSpace' as any,
|
||||||
|
person: 'person-id' as any,
|
||||||
|
name: 'Test Space',
|
||||||
|
private: true
|
||||||
|
} as any
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
|
||||||
|
// Setup mocks
|
||||||
|
mockCtx = {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn()
|
||||||
|
} as unknown as MeasureContext
|
||||||
|
|
||||||
|
mockTxClient = {
|
||||||
|
createDoc: jest.fn().mockResolvedValue('thread-id'),
|
||||||
|
createMixin: jest.fn().mockResolvedValue(undefined),
|
||||||
|
findOne: jest.fn()
|
||||||
|
} as unknown as jest.Mocked<TxOperations>
|
||||||
|
|
||||||
|
mockKvsClient = {} as unknown as jest.Mocked<KeyValueClient>
|
||||||
|
|
||||||
|
mockProducer = {
|
||||||
|
send: jest.fn().mockResolvedValue(undefined)
|
||||||
|
} as unknown as jest.Mocked<Producer>
|
||||||
|
|
||||||
|
mockToken = 'test-token'
|
||||||
|
|
||||||
|
mockWsInfo = {
|
||||||
|
workspace: 'workspace-id',
|
||||||
|
endpoint: 'wss://example.com',
|
||||||
|
token: 'ws-token',
|
||||||
|
workspaceUrl: 'https://example.com',
|
||||||
|
workspaceDataId: 'data-id'
|
||||||
|
} as any
|
||||||
|
|
||||||
|
mockMessage = {
|
||||||
|
mailId: 'test-mail-id',
|
||||||
|
from: mockFromContact,
|
||||||
|
to: [mockToContact1, mockToContact2],
|
||||||
|
copy: [],
|
||||||
|
subject: 'Test Subject',
|
||||||
|
textContent: 'Test Content',
|
||||||
|
htmlContent: '<p>Test Content</p>',
|
||||||
|
sendOn: Date.now()
|
||||||
|
} as any
|
||||||
|
|
||||||
|
mockAttachments = []
|
||||||
|
|
||||||
|
// Setup config
|
||||||
|
mockConfig = {
|
||||||
|
CommunicationTopic: 'communication-topic'
|
||||||
|
} as any
|
||||||
|
|
||||||
|
// Setup mock factories
|
||||||
|
;(PersonCacheFactory.getInstance as jest.Mock).mockReturnValue(mockPersonCache)
|
||||||
|
;(PersonSpacesCacheFactory.getInstance as jest.Mock).mockReturnValue(mockPersonSpacesCache)
|
||||||
|
;(ChannelCacheFactory.getInstance as jest.Mock).mockReturnValue(mockChannelCache)
|
||||||
|
;(ThreadLookupService.getInstance as jest.Mock).mockReturnValue(mockThreadLookup)
|
||||||
|
|
||||||
|
// Setup mock return values
|
||||||
|
mockPersonCache.ensurePerson
|
||||||
|
.mockResolvedValueOnce(mockFromPerson) // For from person
|
||||||
|
.mockResolvedValueOnce(mockToPerson1) // For first to person
|
||||||
|
.mockResolvedValueOnce(mockToPerson2) // For second to person
|
||||||
|
|
||||||
|
mockPersonSpacesCache.getPersonSpaces.mockResolvedValue([mockSpace])
|
||||||
|
|
||||||
|
mockChannelCache.getOrCreateChannel.mockResolvedValue('channel-id')
|
||||||
|
|
||||||
|
mockThreadLookup.getThreadId.mockResolvedValue(undefined)
|
||||||
|
mockThreadLookup.getParentThreadId.mockResolvedValue(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default participants when targetPersons is not provided', async () => {
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
mockMessage,
|
||||||
|
mockAttachments
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// This checks that createDoc was called with the expected participants list
|
||||||
|
// Default participants should include the sender and all recipients
|
||||||
|
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
members: [mockFromPerson.socialId, mockToPerson1.socialId, mockToPerson2.socialId]
|
||||||
|
}),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Number),
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Also check that the channel was created with the same participants
|
||||||
|
expect(mockChannelCache.getOrCreateChannel).toHaveBeenCalledWith(
|
||||||
|
mockSpace._id,
|
||||||
|
[mockFromPerson.socialId, mockToPerson1.socialId, mockToPerson2.socialId],
|
||||||
|
mockToContact1.email,
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use provided targetPersons when specified', async () => {
|
||||||
|
// Arrange
|
||||||
|
const customTargetPersons = ['custom-person-1', 'custom-person-2'] as PersonId[]
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
mockMessage,
|
||||||
|
mockAttachments,
|
||||||
|
customTargetPersons
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Check that createDoc was called with the custom participants list
|
||||||
|
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
members: customTargetPersons
|
||||||
|
}),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Number),
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Also check that the channel was created with the custom participants
|
||||||
|
expect(mockChannelCache.getOrCreateChannel).toHaveBeenCalledWith(
|
||||||
|
mockSpace._id,
|
||||||
|
customTargetPersons,
|
||||||
|
mockToContact1.email,
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty targetPersons array', async () => {
|
||||||
|
// Arrange
|
||||||
|
const emptyTargetPersons: PersonId[] = []
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
mockMessage,
|
||||||
|
mockAttachments,
|
||||||
|
emptyTargetPersons
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Check that createDoc was called with the empty participants list
|
||||||
|
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
members: [] // Empty array
|
||||||
|
}),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Number),
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing recipient persons gracefully', async () => {
|
||||||
|
// Arrange - setup person cache to return undefined for the second recipient
|
||||||
|
mockPersonCache.ensurePerson
|
||||||
|
.mockReset()
|
||||||
|
.mockResolvedValueOnce(mockFromPerson) // For from person
|
||||||
|
.mockResolvedValueOnce(mockToPerson1) // For first to person
|
||||||
|
.mockResolvedValueOnce(undefined) // For second to person (missing)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
mockMessage,
|
||||||
|
mockAttachments
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Only the available persons should be included in participants
|
||||||
|
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
members: [
|
||||||
|
mockFromPerson.socialId,
|
||||||
|
mockToPerson1.socialId
|
||||||
|
// mockToPerson2.socialId should not be here
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Number),
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle case where all recipients are missing', async () => {
|
||||||
|
// Arrange - setup person cache to return undefined for all recipients
|
||||||
|
mockPersonCache.ensurePerson
|
||||||
|
.mockReset()
|
||||||
|
.mockResolvedValueOnce(mockFromPerson) // For from person
|
||||||
|
.mockResolvedValueOnce(undefined) // For first to person (missing)
|
||||||
|
.mockResolvedValueOnce(undefined) // For second to person (missing)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
mockMessage,
|
||||||
|
mockAttachments
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Should log an error and not proceed with message creation
|
||||||
|
expect(mockCtx.error).toHaveBeenCalledWith('Unable to create message without a proper TO', expect.any(Object))
|
||||||
|
expect(mockTxClient.createDoc).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty to list with copy recipients', async () => {
|
||||||
|
// Arrange - setup message with empty 'to' but populated 'copy'
|
||||||
|
const messageWithCopy: EmailMessage = {
|
||||||
|
...mockMessage,
|
||||||
|
to: [],
|
||||||
|
copy: [mockToContact1]
|
||||||
|
}
|
||||||
|
|
||||||
|
mockPersonCache.ensurePerson
|
||||||
|
.mockReset()
|
||||||
|
.mockResolvedValueOnce(mockFromPerson) // For from person
|
||||||
|
.mockResolvedValueOnce(mockToPerson1) // For copy person
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await createMessages(
|
||||||
|
mockConfig,
|
||||||
|
mockCtx,
|
||||||
|
mockTxClient,
|
||||||
|
mockKvsClient,
|
||||||
|
mockProducer,
|
||||||
|
mockToken,
|
||||||
|
mockWsInfo,
|
||||||
|
messageWithCopy,
|
||||||
|
mockAttachments
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Should include the copy recipients in participants
|
||||||
|
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
members: [mockFromPerson.socialId, mockToPerson1.socialId]
|
||||||
|
}),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Number),
|
||||||
|
expect.any(String)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
@ -46,6 +46,32 @@ import { PersonSpacesCacheFactory } from './personSpaces'
|
|||||||
import { ChannelCache, ChannelCacheFactory } from './channel'
|
import { ChannelCache, ChannelCacheFactory } from './channel'
|
||||||
import { ThreadLookupService } from './thread'
|
import { ThreadLookupService } from './thread'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates mail messages in the platform
|
||||||
|
*
|
||||||
|
* This function processes an email message and creates corresponding chat messages. It handles:
|
||||||
|
* - Ensuring persons exist for email addresses
|
||||||
|
* - Finding or creating channels for participants
|
||||||
|
* - Creating threads for messages
|
||||||
|
* - Uploading attachments to storage
|
||||||
|
* - Sending message events to Kafka
|
||||||
|
*
|
||||||
|
* @param {BaseConfig} config - Configuration options including storage and Kafka settings
|
||||||
|
* @param {MeasureContext} ctx - Context for logging and performance measurement
|
||||||
|
* @param {TxOperations} txClient - Client for database transactions
|
||||||
|
* @param {KeyValueClient} keyValueClient - Client for key-value storage operations
|
||||||
|
* @param {Producer} producer - Kafka producer for sending message events
|
||||||
|
* @param {string} token - Authentication token for API calls
|
||||||
|
* @param {WorkspaceLoginInfo} wsInfo - Workspace information including ID and URLs
|
||||||
|
* @param {EmailMessage} message - The email message to process
|
||||||
|
* @param {Attachment[]} attachments - Array of attachments for the message
|
||||||
|
* @param {PersonId[]} [targetPersons] - Optional list of specific persons who should receive the message.
|
||||||
|
* If not provided, all existing accounts from email addresses will be used.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when all messages have been created
|
||||||
|
* @throws Will log errors but not throw exceptions for partial failures
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export async function createMessages (
|
export async function createMessages (
|
||||||
config: BaseConfig,
|
config: BaseConfig,
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
@ -55,7 +81,8 @@ export async function createMessages (
|
|||||||
token: string,
|
token: string,
|
||||||
wsInfo: WorkspaceLoginInfo,
|
wsInfo: WorkspaceLoginInfo,
|
||||||
message: EmailMessage,
|
message: EmailMessage,
|
||||||
attachments: Attachment[]
|
attachments: Attachment[],
|
||||||
|
targetPersons?: PersonId[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { mailId, from, subject, replyTo } = message
|
const { mailId, from, subject, replyTo } = message
|
||||||
const tos = [...(message.to ?? []), ...(message.copy ?? [])]
|
const tos = [...(message.to ?? []), ...(message.copy ?? [])]
|
||||||
@ -82,7 +109,7 @@ export async function createMessages (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modifiedBy = fromPerson.socialId
|
const modifiedBy = fromPerson.socialId
|
||||||
const participants = [fromPerson.socialId, ...toPersons.map((p) => p.socialId)]
|
const participants = targetPersons ?? [fromPerson.socialId, ...toPersons.map((p) => p.socialId)]
|
||||||
const content = getMdContent(ctx, message)
|
const content = getMdContent(ctx, message)
|
||||||
|
|
||||||
const attachedBlobs: Attachment[] = []
|
const attachedBlobs: Attachment[] = []
|
||||||
|
Loading…
Reference in New Issue
Block a user