mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-10 01:15:03 +00:00
UBERF-10408: Use adapter for message manager
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
parent
60003c8d9e
commit
9137ebed2d
@ -789,7 +789,7 @@ importers:
|
|||||||
version: file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.2.15(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
|
version: file:projects/pod-github.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(utf-8-validate@6.0.4)(y-prosemirror@1.2.15(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23))
|
||||||
'@rush-temp/pod-gmail':
|
'@rush-temp/pod-gmail':
|
||||||
specifier: file:./projects/pod-gmail.tgz
|
specifier: file:./projects/pod-gmail.tgz
|
||||||
version: file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)
|
version: file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)
|
||||||
'@rush-temp/pod-gmail-next':
|
'@rush-temp/pod-gmail-next':
|
||||||
specifier: file:./projects/pod-gmail-next.tgz
|
specifier: file:./projects/pod-gmail-next.tgz
|
||||||
version: file:projects/pod-gmail-next.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)
|
version: file:projects/pod-gmail-next.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)
|
||||||
@ -4067,7 +4067,7 @@ packages:
|
|||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/chat-resources@file:projects/chat-resources.tgz':
|
'@rush-temp/chat-resources@file:projects/chat-resources.tgz':
|
||||||
resolution: {integrity: sha512-kP5xHl8xRmxWjmgoSp/TglS3JXlbgsDVLO4quh2Dfz/d1Shzs2dL4g9OptKgJrscMO2OmafNwqIGspjAR/imfw==, tarball: file:projects/chat-resources.tgz}
|
resolution: {integrity: sha512-snQjnHjolp4vMxRX2rxtY6mrmBGVdTQsU/SXtvQxOQYFHCJ12gWHUaAl/61PmRQuKXNZ3OSnulis6kM8BUsOlQ==, tarball: file:projects/chat-resources.tgz}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/chat@file:projects/chat.tgz':
|
'@rush-temp/chat@file:projects/chat.tgz':
|
||||||
@ -4111,7 +4111,7 @@ packages:
|
|||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/communication-resources@file:projects/communication-resources.tgz':
|
'@rush-temp/communication-resources@file:projects/communication-resources.tgz':
|
||||||
resolution: {integrity: sha512-9+OZuK/zjwHSGGBafFODO/vy80mG61lRsTUNHvxcSe/+5SBHJ9jnQP1J7ENaEw3StUebt/4x5fykHaLM5b1J9g==, tarball: file:projects/communication-resources.tgz}
|
resolution: {integrity: sha512-ttk6jX6wYnFbBNRXwQkZ07kU9pE4VFc4gGwfZDOenou7QCSmX6C1Nwz41ZTFLzBKWW6mfEpWJTaCUA0aIIU6NQ==, tarball: file:projects/communication-resources.tgz}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/communication@file:projects/communication.tgz':
|
'@rush-temp/communication@file:projects/communication.tgz':
|
||||||
@ -4459,7 +4459,7 @@ packages:
|
|||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/model-communication@file:projects/model-communication.tgz':
|
'@rush-temp/model-communication@file:projects/model-communication.tgz':
|
||||||
resolution: {integrity: sha512-trMTSOxxb9spxcc/KxQfUFOta6yRbLpOt6tKqPVF2mTaZ9G2/8k8eGcjzTgMEb8ghBk8IQFl5hDkd7LB4vDoyg==, tarball: file:projects/model-communication.tgz}
|
resolution: {integrity: sha512-Fbi79c8xZjCCG892THwn1EHUSA5CecN/DhcyibrsjwWMJP5W4jLnv+2iJhwKHZNZ/bUnRDgKFv5iWUIPu28WDA==, tarball: file:projects/model-communication.tgz}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/model-contact@file:projects/model-contact.tgz':
|
'@rush-temp/model-contact@file:projects/model-contact.tgz':
|
||||||
@ -4863,7 +4863,7 @@ packages:
|
|||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/pod-gmail@file:projects/pod-gmail.tgz':
|
'@rush-temp/pod-gmail@file:projects/pod-gmail.tgz':
|
||||||
resolution: {integrity: sha512-mWpD7TVaoiHBcGFgqt2KoPioV7Uc/9Y7DXu3LKiwZBrUX6X+WR/77RcKq/gNGPbwNxCKOc+dDsscF5NKMhIqSg==, tarball: file:projects/pod-gmail.tgz}
|
resolution: {integrity: sha512-5sqBFg5VB9InzTjRtZOQewE63ntNzDTdE38tjiKPT4e387vjhGP+IWY1l8PUUs1h/DHNyyeqV6SSzmz/x+mYjQ==, tarball: file:projects/pod-gmail.tgz}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/pod-inbound-mail@file:projects/pod-inbound-mail.tgz':
|
'@rush-temp/pod-inbound-mail@file:projects/pod-inbound-mail.tgz':
|
||||||
@ -5547,7 +5547,7 @@ packages:
|
|||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/ui-next@file:projects/ui-next.tgz':
|
'@rush-temp/ui-next@file:projects/ui-next.tgz':
|
||||||
resolution: {integrity: sha512-046zP40TN02FxzfJEdU33B0kfek57fc0XwlvWaPn2n/u2UVwihzCt22/a6Q+JjOe11g14CrgqQbHxgb88eRRbA==, tarball: file:projects/ui-next.tgz}
|
resolution: {integrity: sha512-xBYAZFZo/fNykUlBjx2UwSbOZqsh3ilzIsfTsFttS01+WqUIZGRHs1Z9bsSk75sOXqYx86C7il+662gFFj5dSg==, tarball: file:projects/ui-next.tgz}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|
||||||
'@rush-temp/ui@file:projects/ui.tgz':
|
'@rush-temp/ui@file:projects/ui.tgz':
|
||||||
@ -22341,7 +22341,7 @@ snapshots:
|
|||||||
- node-notifier
|
- node-notifier
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@rush-temp/pod-gmail@file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)':
|
'@rush-temp/pod-gmail@file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tsconfig/node16': 1.0.4
|
'@tsconfig/node16': 1.0.4
|
||||||
'@types/cors': 2.8.17
|
'@types/cors': 2.8.17
|
||||||
@ -22349,6 +22349,7 @@ snapshots:
|
|||||||
'@types/jest': 29.5.12
|
'@types/jest': 29.5.12
|
||||||
'@types/node': 20.11.19
|
'@types/node': 20.11.19
|
||||||
'@types/sanitize-html': 2.15.0
|
'@types/sanitize-html': 2.15.0
|
||||||
|
'@types/turndown': 5.0.5
|
||||||
'@types/uuid': 8.3.4
|
'@types/uuid': 8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
|
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
|
||||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||||
@ -22370,30 +22371,23 @@ snapshots:
|
|||||||
googleapis: 122.0.0(encoding@0.1.13)
|
googleapis: 122.0.0(encoding@0.1.13)
|
||||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
||||||
jwt-simple: 0.5.6
|
jwt-simple: 0.5.6
|
||||||
mongodb: 6.12.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)
|
|
||||||
prettier: 3.2.5
|
prettier: 3.2.5
|
||||||
sanitize-html: 2.16.0
|
sanitize-html: 2.16.0
|
||||||
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
|
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
|
||||||
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
||||||
ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3)
|
ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3)
|
||||||
|
turndown: 7.2.0
|
||||||
typescript: 5.3.3
|
typescript: 5.3.3
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@jest/types'
|
- '@jest/types'
|
||||||
- '@mongodb-js/zstd'
|
|
||||||
- '@swc/core'
|
- '@swc/core'
|
||||||
- '@swc/wasm'
|
- '@swc/wasm'
|
||||||
- babel-jest
|
- babel-jest
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
|
||||||
- kerberos
|
|
||||||
- mongodb-client-encryption
|
|
||||||
- node-notifier
|
- node-notifier
|
||||||
- snappy
|
|
||||||
- socks
|
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@rush-temp/pod-inbound-mail@file:projects/pod-inbound-mail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
|
'@rush-temp/pod-inbound-mail@file:projects/pod-inbound-mail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"_phase:bundle": "rushx bundle",
|
"_phase:bundle": "rushx bundle",
|
||||||
"_phase:docker-build": "rushx docker:build",
|
"_phase:docker-build": "rushx docker:build",
|
||||||
"_phase:docker-staging": "rushx docker:staging",
|
"_phase:docker-staging": "rushx docker:staging",
|
||||||
"bundle": "node ../../../common/scripts/esbuild.js",
|
"bundle": "node ../../../common/scripts/esbuild.js --external=ws",
|
||||||
"docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/gmail",
|
"docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/gmail",
|
||||||
"docker:tbuild": "docker build -t hardcoreeng/gmail . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/gmail",
|
"docker:tbuild": "docker build -t hardcoreeng/gmail . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/gmail",
|
||||||
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/gmail staging",
|
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/gmail staging",
|
||||||
@ -36,6 +36,7 @@
|
|||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/node": "~20.11.16",
|
"@types/node": "~20.11.16",
|
||||||
"@types/sanitize-html": "^2.15.0",
|
"@types/sanitize-html": "^2.15.0",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
"@typescript-eslint/parser": "^6.11.0",
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
"esbuild": "^0.24.2",
|
"esbuild": "^0.24.2",
|
||||||
@ -58,10 +59,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/attachment": "^0.6.14",
|
"@hcengineering/attachment": "^0.6.14",
|
||||||
"@hcengineering/account-client": "^0.6.0",
|
"@hcengineering/account-client": "^0.6.0",
|
||||||
|
"@hcengineering/api-client": "^0.6.0",
|
||||||
|
"@hcengineering/card": "^0.6.0",
|
||||||
|
"@hcengineering/chat": "^0.6.0",
|
||||||
"@hcengineering/client": "^0.6.18",
|
"@hcengineering/client": "^0.6.18",
|
||||||
"@hcengineering/client-resources": "^0.6.27",
|
"@hcengineering/client-resources": "^0.6.27",
|
||||||
"@hcengineering/contact": "^0.6.24",
|
"@hcengineering/contact": "^0.6.24",
|
||||||
"@hcengineering/mongo": "^0.6.1",
|
"@hcengineering/mail-common": "^0.6.0",
|
||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
"@hcengineering/gmail": "^0.6.22",
|
"@hcengineering/gmail": "^0.6.22",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
@ -80,9 +84,9 @@
|
|||||||
"google-auth-library": "^8.0.2",
|
"google-auth-library": "^8.0.2",
|
||||||
"gaxios": "^5.0.1",
|
"gaxios": "^5.0.1",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"mongodb": "^6.12.0",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"@hcengineering/analytics-service": "^0.6.0",
|
"@hcengineering/analytics-service": "^0.6.0",
|
||||||
"sanitize-html": "^2.15.0"
|
"sanitize-html": "^2.15.0",
|
||||||
|
"turndown": "^7.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import { StorageAdapter } from '@hcengineering/server-core'
|
import { StorageAdapter } from '@hcengineering/server-core'
|
||||||
import { gmail_v1 } from 'googleapis'
|
import { gmail_v1 } from 'googleapis'
|
||||||
import { AttachmentHandler } from '../message/attachments'
|
import { AttachmentHandler } from '../message/attachments'
|
||||||
import type { AttachedFile } from '../types'
|
import type { Attachment as AttachedFile } from '@hcengineering/mail-common'
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { decode64, encode64 } from '../base64'
|
import { decode64, encode64 } from '../base64'
|
||||||
|
|
||||||
@ -83,9 +83,10 @@ describe('AttachmentHandler', () => {
|
|||||||
describe('addAttachement', () => {
|
describe('addAttachement', () => {
|
||||||
it('should not add attachment if it already exists', async () => {
|
it('should not add attachment if it already exists', async () => {
|
||||||
const file: AttachedFile = {
|
const file: AttachedFile = {
|
||||||
file: 'test-file',
|
id: 'test-id',
|
||||||
|
data: Buffer.from('test-file'),
|
||||||
name: 'test.txt',
|
name: 'test.txt',
|
||||||
type: 'text/plain',
|
contentType: 'text/plain',
|
||||||
size: 100,
|
size: 100,
|
||||||
lastModified: Date.now()
|
lastModified: Date.now()
|
||||||
}
|
}
|
||||||
@ -106,9 +107,10 @@ describe('AttachmentHandler', () => {
|
|||||||
|
|
||||||
it('should add new attachment', async () => {
|
it('should add new attachment', async () => {
|
||||||
const file: AttachedFile = {
|
const file: AttachedFile = {
|
||||||
file: 'test-file',
|
id: 'test-id',
|
||||||
|
data: Buffer.from('test-file'),
|
||||||
name: 'test.txt',
|
name: 'test.txt',
|
||||||
type: 'text/plain',
|
contentType: 'text/plain',
|
||||||
size: 100,
|
size: 100,
|
||||||
lastModified: Date.now()
|
lastModified: Date.now()
|
||||||
}
|
}
|
||||||
@ -154,9 +156,10 @@ describe('AttachmentHandler', () => {
|
|||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
file: 'test-data',
|
id: expect.any(String),
|
||||||
|
data: expect.any(Buffer),
|
||||||
name: 'test.txt',
|
name: 'test.txt',
|
||||||
type: 'text/plain',
|
contentType: 'text/plain',
|
||||||
size: 100,
|
size: 100,
|
||||||
lastModified: expect.any(Number)
|
lastModified: expect.any(Number)
|
||||||
}
|
}
|
||||||
@ -177,9 +180,10 @@ describe('AttachmentHandler', () => {
|
|||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
file: 'test-data',
|
id: expect.any(String),
|
||||||
|
data: expect.any(Buffer),
|
||||||
name: 'test.txt',
|
name: 'test.txt',
|
||||||
type: 'text/plain',
|
contentType: 'text/plain',
|
||||||
size: 100,
|
size: 100,
|
||||||
lastModified: expect.any(Number)
|
lastModified: expect.any(Number)
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,10 @@ jest.mock('@hcengineering/core', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
jest.mock('@hcengineering/mail-common', () => ({
|
||||||
|
createMessages: jest.fn().mockResolvedValue(undefined)
|
||||||
|
}))
|
||||||
|
|
||||||
jest.mock('googleapis', () => ({
|
jest.mock('googleapis', () => ({
|
||||||
gmail_v1: {},
|
gmail_v1: {},
|
||||||
google: {
|
google: {
|
||||||
@ -69,7 +73,7 @@ jest.mock('googleapis', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('../tokens')
|
jest.mock('../tokens')
|
||||||
jest.mock('../message/message')
|
jest.mock('../message/adapter')
|
||||||
jest.mock('../message/sync')
|
jest.mock('../message/sync')
|
||||||
jest.mock('../message/attachments')
|
jest.mock('../message/attachments')
|
||||||
jest.mock('@hcengineering/server-client', () => ({
|
jest.mock('@hcengineering/server-client', () => ({
|
||||||
|
155
services/gmail/pod-gmail/src/__tests__/message-v1.test.ts
Normal file
155
services/gmail/pod-gmail/src/__tests__/message-v1.test.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { type MeasureContext, PersonId, TxOperations, AttachedData } from '@hcengineering/core'
|
||||||
|
import { type GaxiosResponse } from 'gaxios'
|
||||||
|
import { gmail_v1 } from 'googleapis'
|
||||||
|
import { type Message } from '@hcengineering/gmail'
|
||||||
|
|
||||||
|
import { type Channel } from '../types'
|
||||||
|
import { AttachmentHandler } from '../message/attachments'
|
||||||
|
import { MessageManagerV1 } from '../message/v1/message'
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
|
||||||
|
jest.mock('../config')
|
||||||
|
|
||||||
|
describe('MessageManager', () => {
|
||||||
|
let messageManager: MessageManagerV1
|
||||||
|
let mockCtx: MeasureContext
|
||||||
|
let mockClient: TxOperations
|
||||||
|
let mockAttachmentHandler: AttachmentHandler
|
||||||
|
let mockSocialId: PersonId
|
||||||
|
let mockWorkspace: { getChannel: (email: string) => Channel | undefined }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCtx = {
|
||||||
|
measure: jest.fn(),
|
||||||
|
with: jest.fn()
|
||||||
|
} as unknown as MeasureContext
|
||||||
|
|
||||||
|
mockClient = {
|
||||||
|
findOne: jest.fn(),
|
||||||
|
findAll: jest.fn(),
|
||||||
|
tx: jest.fn()
|
||||||
|
} as unknown as TxOperations
|
||||||
|
|
||||||
|
mockAttachmentHandler = {
|
||||||
|
getPartFiles: jest.fn(),
|
||||||
|
addAttachement: jest.fn()
|
||||||
|
} as unknown as AttachmentHandler
|
||||||
|
|
||||||
|
mockSocialId = 'test-social-id' as PersonId
|
||||||
|
|
||||||
|
mockWorkspace = {
|
||||||
|
getChannel: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
messageManager = new MessageManagerV1(mockCtx, mockClient, mockAttachmentHandler, mockSocialId, mockWorkspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('saveMessage', () => {
|
||||||
|
const createMockGmailResponse = (): GaxiosResponse<gmail_v1.Schema$Message> => ({
|
||||||
|
config: {},
|
||||||
|
data: {
|
||||||
|
id: 'test-message-id',
|
||||||
|
internalDate: '1234567890',
|
||||||
|
payload: {
|
||||||
|
headers: [
|
||||||
|
{ name: 'From', value: 'sender@example.com' },
|
||||||
|
{ name: 'To', value: 'recipient@example.com' },
|
||||||
|
{ name: 'Message-ID', value: 'test-message-id' },
|
||||||
|
{ name: 'Subject', value: 'Test Subject' }
|
||||||
|
],
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
body: { data: 'dGVzdCBjb250ZW50' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mimeType: 'text/html',
|
||||||
|
body: { data: 'PHA+dGVzdCBjb250ZW50PC9wPg==' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
headers: {},
|
||||||
|
request: {
|
||||||
|
responseURL: 'https://example.com'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMockMessage = (): AttachedData<Message> => ({
|
||||||
|
messageId: 'test-message-id',
|
||||||
|
textContent: 'test content',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
content: 'test content',
|
||||||
|
sendOn: 1234567890,
|
||||||
|
from: 'sender@example.com',
|
||||||
|
to: 'recipient@example.com',
|
||||||
|
copy: [],
|
||||||
|
incoming: true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should save message with attachments', async () => {
|
||||||
|
const mockMessage = createMockGmailResponse()
|
||||||
|
const mockChannel = { _id: 'test-channel-id' } as Channel
|
||||||
|
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
||||||
|
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
||||||
|
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
||||||
|
|
||||||
|
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
||||||
|
|
||||||
|
expect(mockClient.tx).toHaveBeenCalled()
|
||||||
|
expect(mockAttachmentHandler.getPartFiles).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update existing message', async () => {
|
||||||
|
const mockMessage = createMockGmailResponse()
|
||||||
|
const mockChannel = { _id: 'test-channel-id' } as Channel
|
||||||
|
const existingMessage = createMockMessage()
|
||||||
|
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
||||||
|
mockClient.findOne = jest.fn().mockResolvedValue(existingMessage)
|
||||||
|
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
||||||
|
|
||||||
|
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
||||||
|
|
||||||
|
expect(mockClient.tx).toHaveBeenCalled()
|
||||||
|
expect(mockAttachmentHandler.getPartFiles).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not save message when no channels found', async () => {
|
||||||
|
const mockMessage = createMockGmailResponse()
|
||||||
|
mockWorkspace.getChannel = jest.fn().mockReturnValue(undefined)
|
||||||
|
|
||||||
|
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
||||||
|
|
||||||
|
expect(mockClient.tx).not.toHaveBeenCalled()
|
||||||
|
expect(mockAttachmentHandler.getPartFiles).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle incoming messages correctly', async () => {
|
||||||
|
const mockMessage = createMockGmailResponse()
|
||||||
|
const mockChannel = { _id: 'test-channel-id' } as Channel
|
||||||
|
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
||||||
|
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
||||||
|
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
||||||
|
|
||||||
|
await messageManager.saveMessage(mockMessage, 'recipient@example.com')
|
||||||
|
|
||||||
|
expect(mockWorkspace.getChannel).toHaveBeenCalledWith('sender@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle outgoing messages correctly', async () => {
|
||||||
|
const mockMessage = createMockGmailResponse()
|
||||||
|
const mockChannel = { _id: 'test-channel-id' } as Channel
|
||||||
|
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
||||||
|
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
||||||
|
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
||||||
|
|
||||||
|
await messageManager.saveMessage(mockMessage, 'sender@example.com')
|
||||||
|
|
||||||
|
expect(mockWorkspace.getChannel).toHaveBeenCalledWith('recipient@example.com')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,159 +1,10 @@
|
|||||||
import { type MeasureContext, PersonId, TxOperations, AttachedData } from '@hcengineering/core'
|
import { sanitizeText } from '../message/v2/message'
|
||||||
import { type GaxiosResponse } from 'gaxios'
|
|
||||||
import { gmail_v1 } from 'googleapis'
|
|
||||||
import { type Message } from '@hcengineering/gmail'
|
|
||||||
|
|
||||||
import { type Channel } from '../types'
|
|
||||||
import { AttachmentHandler } from '../message/attachments'
|
|
||||||
import { MessageManager, sanitizeText } from '../message/message'
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
|
||||||
jest.mock('../config')
|
jest.mock('../config')
|
||||||
|
|
||||||
describe('MessageManager', () => {
|
|
||||||
let messageManager: MessageManager
|
|
||||||
let mockCtx: MeasureContext
|
|
||||||
let mockClient: TxOperations
|
|
||||||
let mockAttachmentHandler: AttachmentHandler
|
|
||||||
let mockSocialId: PersonId
|
|
||||||
let mockWorkspace: { getChannel: (email: string) => Channel | undefined }
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockCtx = {
|
|
||||||
measure: jest.fn(),
|
|
||||||
with: jest.fn()
|
|
||||||
} as unknown as MeasureContext
|
|
||||||
|
|
||||||
mockClient = {
|
|
||||||
findOne: jest.fn(),
|
|
||||||
findAll: jest.fn(),
|
|
||||||
tx: jest.fn()
|
|
||||||
} as unknown as TxOperations
|
|
||||||
|
|
||||||
mockAttachmentHandler = {
|
|
||||||
getPartFiles: jest.fn(),
|
|
||||||
addAttachement: jest.fn()
|
|
||||||
} as unknown as AttachmentHandler
|
|
||||||
|
|
||||||
mockSocialId = 'test-social-id' as PersonId
|
|
||||||
|
|
||||||
mockWorkspace = {
|
|
||||||
getChannel: jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
messageManager = new MessageManager(mockCtx, mockClient, mockAttachmentHandler, mockSocialId, mockWorkspace)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('saveMessage', () => {
|
|
||||||
const createMockGmailResponse = (): GaxiosResponse<gmail_v1.Schema$Message> => ({
|
|
||||||
config: {},
|
|
||||||
data: {
|
|
||||||
id: 'test-message-id',
|
|
||||||
internalDate: '1234567890',
|
|
||||||
payload: {
|
|
||||||
headers: [
|
|
||||||
{ name: 'From', value: 'sender@example.com' },
|
|
||||||
{ name: 'To', value: 'recipient@example.com' },
|
|
||||||
{ name: 'Message-ID', value: 'test-message-id' },
|
|
||||||
{ name: 'Subject', value: 'Test Subject' }
|
|
||||||
],
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
mimeType: 'text/plain',
|
|
||||||
body: { data: 'dGVzdCBjb250ZW50' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mimeType: 'text/html',
|
|
||||||
body: { data: 'PHA+dGVzdCBjb250ZW50PC9wPg==' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
headers: {},
|
|
||||||
request: {
|
|
||||||
responseURL: 'https://example.com'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const createMockMessage = (): AttachedData<Message> => ({
|
|
||||||
messageId: 'test-message-id',
|
|
||||||
textContent: 'test content',
|
|
||||||
subject: 'Test Subject',
|
|
||||||
content: 'test content',
|
|
||||||
sendOn: 1234567890,
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
copy: [],
|
|
||||||
incoming: true
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should save message with attachments', async () => {
|
|
||||||
const mockMessage = createMockGmailResponse()
|
|
||||||
const mockChannel = { _id: 'test-channel-id' } as Channel
|
|
||||||
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
|
||||||
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
|
||||||
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
|
||||||
|
|
||||||
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
|
||||||
|
|
||||||
expect(mockClient.tx).toHaveBeenCalled()
|
|
||||||
expect(mockAttachmentHandler.getPartFiles).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update existing message', async () => {
|
|
||||||
const mockMessage = createMockGmailResponse()
|
|
||||||
const mockChannel = { _id: 'test-channel-id' } as Channel
|
|
||||||
const existingMessage = createMockMessage()
|
|
||||||
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
|
||||||
mockClient.findOne = jest.fn().mockResolvedValue(existingMessage)
|
|
||||||
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
|
||||||
|
|
||||||
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
|
||||||
|
|
||||||
expect(mockClient.tx).toHaveBeenCalled()
|
|
||||||
expect(mockAttachmentHandler.getPartFiles).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not save message when no channels found', async () => {
|
|
||||||
const mockMessage = createMockGmailResponse()
|
|
||||||
mockWorkspace.getChannel = jest.fn().mockReturnValue(undefined)
|
|
||||||
|
|
||||||
await messageManager.saveMessage(mockMessage, 'test@example.com')
|
|
||||||
|
|
||||||
expect(mockClient.tx).not.toHaveBeenCalled()
|
|
||||||
expect(mockAttachmentHandler.getPartFiles).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle incoming messages correctly', async () => {
|
|
||||||
const mockMessage = createMockGmailResponse()
|
|
||||||
const mockChannel = { _id: 'test-channel-id' } as Channel
|
|
||||||
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
|
||||||
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
|
||||||
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
|
||||||
|
|
||||||
await messageManager.saveMessage(mockMessage, 'recipient@example.com')
|
|
||||||
|
|
||||||
expect(mockWorkspace.getChannel).toHaveBeenCalledWith('sender@example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle outgoing messages correctly', async () => {
|
|
||||||
const mockMessage = createMockGmailResponse()
|
|
||||||
const mockChannel = { _id: 'test-channel-id' } as Channel
|
|
||||||
mockWorkspace.getChannel = jest.fn().mockReturnValue(mockChannel)
|
|
||||||
mockClient.findOne = jest.fn().mockResolvedValue(undefined)
|
|
||||||
mockAttachmentHandler.getPartFiles = jest.fn().mockResolvedValue([])
|
|
||||||
|
|
||||||
await messageManager.saveMessage(mockMessage, 'sender@example.com')
|
|
||||||
|
|
||||||
expect(mockWorkspace.getChannel).toHaveBeenCalledWith('recipient@example.com')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('sanitizeHtml', () => {
|
describe('sanitizeHtml', () => {
|
||||||
it('should remove all HTML tags', () => {
|
it('should remove all HTML tags', () => {
|
||||||
const html = '<p>This is <b>bold</b> and <i>italic</i> text</p>'
|
const html = '<p>This is <b>bold</b> and <i>italic</i> text</p>'
|
||||||
|
@ -13,12 +13,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { MeasureContext, PersonId } from '@hcengineering/core'
|
import { MeasureContext, PersonId } from '@hcengineering/core'
|
||||||
import { SyncManager, SyncMutex } from '../message/sync'
|
import { SyncManager } from '../message/sync'
|
||||||
import { MessageManager } from '../message/message'
|
import { MessageManagerV2 } from '../message/v2/message'
|
||||||
import { RateLimiter } from '../rateLimiter'
|
import { RateLimiter } from '../rateLimiter'
|
||||||
|
|
||||||
jest.mock('../config')
|
jest.mock('../config')
|
||||||
jest.mock('../message/message')
|
jest.mock('../message/adapter')
|
||||||
jest.mock('../utils', () => {
|
jest.mock('../utils', () => {
|
||||||
const originalModule = jest.requireActual('../utils')
|
const originalModule = jest.requireActual('../utils')
|
||||||
return {
|
return {
|
||||||
@ -46,7 +46,7 @@ const mockKeyValueClientInstance = {
|
|||||||
describe('SyncManager', () => {
|
describe('SyncManager', () => {
|
||||||
// Mocked dependencies
|
// Mocked dependencies
|
||||||
let mockCtx: MeasureContext
|
let mockCtx: MeasureContext
|
||||||
let mockMessageManager: jest.Mocked<MessageManager>
|
let mockMessageManager: jest.Mocked<MessageManagerV2>
|
||||||
let mockGmail: any // Using any for easier mocking
|
let mockGmail: any // Using any for easier mocking
|
||||||
let mockKeyValueClient: MockKeyValueClient
|
let mockKeyValueClient: MockKeyValueClient
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ describe('SyncManager', () => {
|
|||||||
|
|
||||||
mockMessageManager = {
|
mockMessageManager = {
|
||||||
saveMessage: jest.fn().mockResolvedValue(undefined)
|
saveMessage: jest.fn().mockResolvedValue(undefined)
|
||||||
} as unknown as jest.Mocked<MessageManager>
|
} as unknown as jest.Mocked<MessageManagerV2>
|
||||||
|
|
||||||
mockGmail = {
|
mockGmail = {
|
||||||
history: {
|
history: {
|
||||||
@ -237,101 +237,3 @@ describe('SyncManager', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SyncMutex', () => {
|
|
||||||
let syncMutex: SyncMutex
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
syncMutex = new SyncMutex()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow sequential locking and unlocking', async () => {
|
|
||||||
// Lock first time
|
|
||||||
const release1 = await syncMutex.lock('test-key')
|
|
||||||
release1()
|
|
||||||
|
|
||||||
// Lock second time
|
|
||||||
const release2 = await syncMutex.lock('test-key')
|
|
||||||
release2()
|
|
||||||
|
|
||||||
// If we got here without hanging, the test passes
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should queue up multiple requests for the same key', async () => {
|
|
||||||
const results: number[] = []
|
|
||||||
|
|
||||||
// Start 3 concurrent lock operations
|
|
||||||
const promise1 = (async () => {
|
|
||||||
const release = await syncMutex.lock('test-key')
|
|
||||||
results.push(1)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
release()
|
|
||||||
})()
|
|
||||||
|
|
||||||
const promise2 = (async () => {
|
|
||||||
const release = await syncMutex.lock('test-key')
|
|
||||||
results.push(2)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
||||||
release()
|
|
||||||
})()
|
|
||||||
|
|
||||||
const promise3 = (async () => {
|
|
||||||
const release = await syncMutex.lock('test-key')
|
|
||||||
results.push(3)
|
|
||||||
release()
|
|
||||||
})()
|
|
||||||
|
|
||||||
// Wait for all promises to resolve
|
|
||||||
await Promise.all([promise1, promise2, promise3])
|
|
||||||
|
|
||||||
// The operations should have happened in order
|
|
||||||
expect(results).toEqual([1, 2, 3])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow concurrent operations on different keys', async () => {
|
|
||||||
const results: string[] = []
|
|
||||||
|
|
||||||
// Lock two different keys concurrently
|
|
||||||
const promise1 = (async () => {
|
|
||||||
const release = await syncMutex.lock('key1')
|
|
||||||
results.push('key1-locked')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
||||||
results.push('key1-unlocked')
|
|
||||||
release()
|
|
||||||
})()
|
|
||||||
|
|
||||||
const promise2 = (async () => {
|
|
||||||
const release = await syncMutex.lock('key2')
|
|
||||||
results.push('key2-locked')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
results.push('key2-unlocked')
|
|
||||||
release()
|
|
||||||
})()
|
|
||||||
|
|
||||||
// Wait for both promises to resolve
|
|
||||||
await Promise.all([promise1, promise2])
|
|
||||||
|
|
||||||
// key2 operations should complete before key1 due to shorter timeout
|
|
||||||
expect(results.indexOf('key2-locked')).toBeLessThan(results.indexOf('key1-unlocked'))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should release the lock properly even if an error occurs', async () => {
|
|
||||||
try {
|
|
||||||
const release = await syncMutex.lock('test-key')
|
|
||||||
try {
|
|
||||||
throw new Error('Test error')
|
|
||||||
} finally {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore the error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be able to acquire the lock again
|
|
||||||
const release = await syncMutex.lock('test-key')
|
|
||||||
release()
|
|
||||||
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
@ -13,20 +13,20 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
import { BaseConfig } from '@hcengineering/mail-common'
|
||||||
import { config as dotenvConfig } from 'dotenv'
|
import { config as dotenvConfig } from 'dotenv'
|
||||||
|
|
||||||
dotenvConfig()
|
dotenvConfig()
|
||||||
|
|
||||||
interface Config {
|
interface Config extends BaseConfig {
|
||||||
Port: number
|
Port: number
|
||||||
AccountsURL: string
|
|
||||||
ServiceID: string
|
ServiceID: string
|
||||||
Secret: string
|
Secret: string
|
||||||
Credentials: string
|
Credentials: string
|
||||||
WATCH_TOPIC_NAME: string
|
WATCH_TOPIC_NAME: string
|
||||||
FooterMessage: string
|
FooterMessage: string
|
||||||
InitLimit: number
|
InitLimit: number
|
||||||
KvsUrl: string
|
Version: 'v1' | 'v2'
|
||||||
}
|
}
|
||||||
|
|
||||||
const envMap: { [key in keyof Config]: string } = {
|
const envMap: { [key in keyof Config]: string } = {
|
||||||
@ -38,12 +38,18 @@ const envMap: { [key in keyof Config]: string } = {
|
|||||||
WATCH_TOPIC_NAME: 'WATCH_TOPIC_NAME',
|
WATCH_TOPIC_NAME: 'WATCH_TOPIC_NAME',
|
||||||
FooterMessage: 'FOOTER_MESSAGE',
|
FooterMessage: 'FOOTER_MESSAGE',
|
||||||
InitLimit: 'INIT_LIMIT',
|
InitLimit: 'INIT_LIMIT',
|
||||||
KvsUrl: 'KVS_URL'
|
KvsUrl: 'KVS_URL',
|
||||||
|
StorageConfig: 'STORAGE_CONFIG',
|
||||||
|
Version: 'VERSION'
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||||
|
|
||||||
const config: Config = (() => {
|
const config: Config = (() => {
|
||||||
|
const version = process.env[envMap.Version] ?? 'v1'
|
||||||
|
if (version !== 'v1' && version !== 'v2') {
|
||||||
|
throw new Error(`Invalid version: ${version}. Must be 'v1' or 'v2'.`)
|
||||||
|
}
|
||||||
const params: Partial<Config> = {
|
const params: Partial<Config> = {
|
||||||
Port: parseNumber(process.env[envMap.Port]) ?? 8087,
|
Port: parseNumber(process.env[envMap.Port]) ?? 8087,
|
||||||
AccountsURL: process.env[envMap.AccountsURL],
|
AccountsURL: process.env[envMap.AccountsURL],
|
||||||
@ -53,7 +59,9 @@ const config: Config = (() => {
|
|||||||
WATCH_TOPIC_NAME: process.env[envMap.WATCH_TOPIC_NAME],
|
WATCH_TOPIC_NAME: process.env[envMap.WATCH_TOPIC_NAME],
|
||||||
InitLimit: parseNumber(process.env[envMap.InitLimit]) ?? 50,
|
InitLimit: parseNumber(process.env[envMap.InitLimit]) ?? 50,
|
||||||
FooterMessage: process.env[envMap.FooterMessage] ?? '<br><br><p>Sent via <a href="https://huly.io">Huly</a></p>',
|
FooterMessage: process.env[envMap.FooterMessage] ?? '<br><br><p>Sent via <a href="https://huly.io">Huly</a></p>',
|
||||||
KvsUrl: process.env[envMap.KvsUrl]
|
KvsUrl: process.env[envMap.KvsUrl],
|
||||||
|
StorageConfig: process.env[envMap.StorageConfig],
|
||||||
|
Version: version
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingEnv = (Object.keys(params) as Array<keyof Config>)
|
const missingEnv = (Object.keys(params) as Array<keyof Config>)
|
||||||
|
@ -30,10 +30,11 @@ import { getOrCreateSocialId } from './accounts'
|
|||||||
import { createIntegrationIfNotEsixts, disableIntegration, removeIntegration } from './integrations'
|
import { createIntegrationIfNotEsixts, disableIntegration, removeIntegration } from './integrations'
|
||||||
import { AttachmentHandler } from './message/attachments'
|
import { AttachmentHandler } from './message/attachments'
|
||||||
import { TokenStorage } from './tokens'
|
import { TokenStorage } from './tokens'
|
||||||
import { MessageManager } from './message/message'
|
import { createMessageManager } from './message/adapter'
|
||||||
import { SyncManager } from './message/sync'
|
import { SyncManager } from './message/sync'
|
||||||
import { getEmail } from './gmail/utils'
|
import { getEmail } from './gmail/utils'
|
||||||
import { Integration } from '@hcengineering/account-client'
|
import { Integration } from '@hcengineering/account-client'
|
||||||
|
import { IMessageManager } from './message/types'
|
||||||
|
|
||||||
const SCOPES = ['https://www.googleapis.com/auth/gmail.modify']
|
const SCOPES = ['https://www.googleapis.com/auth/gmail.modify']
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ export class GmailClient {
|
|||||||
private refreshTimer: NodeJS.Timeout | undefined = undefined
|
private refreshTimer: NodeJS.Timeout | undefined = undefined
|
||||||
private readonly rateLimiter = new RateLimiter(1000, 200)
|
private readonly rateLimiter = new RateLimiter(1000, 200)
|
||||||
private readonly attachmentHandler: AttachmentHandler
|
private readonly attachmentHandler: AttachmentHandler
|
||||||
private readonly messageManager: MessageManager
|
private readonly messageManager: IMessageManager
|
||||||
private readonly syncManager: SyncManager
|
private readonly syncManager: SyncManager
|
||||||
private readonly integrationToken: string
|
private readonly integrationToken: string
|
||||||
private integration: Integration | undefined = undefined
|
private integration: Integration | undefined = undefined
|
||||||
@ -104,12 +105,13 @@ export class GmailClient {
|
|||||||
this.client = new TxOperations(client, this.socialId._id)
|
this.client = new TxOperations(client, this.socialId._id)
|
||||||
this.account = this.user.userId
|
this.account = this.user.userId
|
||||||
this.attachmentHandler = new AttachmentHandler(ctx, workspaceId, storageAdapter, this.gmail, this.client)
|
this.attachmentHandler = new AttachmentHandler(ctx, workspaceId, storageAdapter, this.gmail, this.client)
|
||||||
this.messageManager = new MessageManager(
|
this.messageManager = createMessageManager(
|
||||||
ctx,
|
ctx,
|
||||||
this.client,
|
this.client,
|
||||||
this.attachmentHandler,
|
this.attachmentHandler,
|
||||||
this.socialId._id,
|
this.workspace,
|
||||||
this.workspace
|
this.integrationToken,
|
||||||
|
socialId
|
||||||
)
|
)
|
||||||
const keyValueClient = getKvsClient(this.integrationToken)
|
const keyValueClient = getKvsClient(this.integrationToken)
|
||||||
this.syncManager = new SyncManager(
|
this.syncManager = new SyncManager(
|
||||||
|
38
services/gmail/pod-gmail/src/message/adapter.ts
Normal file
38
services/gmail/pod-gmail/src/message/adapter.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// 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 { MeasureContext, TxOperations, SocialId } from '@hcengineering/core'
|
||||||
|
|
||||||
|
import config from '../config'
|
||||||
|
import { AttachmentHandler } from './attachments'
|
||||||
|
import { MessageManagerV2 } from './v2/message'
|
||||||
|
import { MessageManagerV1 } from './v1/message'
|
||||||
|
import { type IMessageManager } from './types'
|
||||||
|
import { type Channel } from '../types'
|
||||||
|
|
||||||
|
export function createMessageManager (
|
||||||
|
ctx: MeasureContext,
|
||||||
|
client: TxOperations,
|
||||||
|
attachmentHandler: AttachmentHandler,
|
||||||
|
workspace: { getChannel: (email: string) => Channel | undefined },
|
||||||
|
token: string,
|
||||||
|
socialId: SocialId
|
||||||
|
): IMessageManager {
|
||||||
|
if (config.Version === 'v2') {
|
||||||
|
return new MessageManagerV2(ctx, attachmentHandler, token, socialId)
|
||||||
|
} else {
|
||||||
|
return new MessageManagerV1(ctx, client, attachmentHandler, socialId._id, workspace)
|
||||||
|
}
|
||||||
|
}
|
@ -12,13 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { AttachedData, Blob, MeasureContext, Ref, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
import { AttachedData, Blob, MeasureContext, Ref, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||||
import { StorageAdapter } from '@hcengineering/server-core'
|
import { StorageAdapter } from '@hcengineering/server-core'
|
||||||
import { gmail_v1 } from 'googleapis'
|
import { gmail_v1 } from 'googleapis'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { encode64 } from '../base64'
|
import { encode64 } from '../base64'
|
||||||
import type { AttachedFile } from '../types'
|
import type { AttachedFile } from './types'
|
||||||
import { addFooter } from '../utils'
|
import { addFooter } from '../utils'
|
||||||
|
|
||||||
export class AttachmentHandler {
|
export class AttachmentHandler {
|
||||||
@ -39,11 +40,11 @@ export class AttachmentHandler {
|
|||||||
const data: AttachedData<Attachment> = {
|
const data: AttachedData<Attachment> = {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
file: id as Ref<Blob>,
|
file: id as Ref<Blob>,
|
||||||
type: file.type ?? 'undefined',
|
type: file.data.toString('base64') ?? 'undefined',
|
||||||
size: file.size ?? Buffer.from(file.file, 'base64').length,
|
size: file.size ?? file.data.length,
|
||||||
lastModified: file.lastModified
|
lastModified: file.lastModified
|
||||||
}
|
}
|
||||||
await this.storageAdapter.put(this.ctx, this.workspaceId as any, id, file.file, data.type, data.size) // TODO: FIXME
|
await this.storageAdapter.put(this.ctx, this.workspaceId as any, id, file.data, data.type, data.size) // TODO: FIXME
|
||||||
await this.client.addCollection(
|
await this.client.addCollection(
|
||||||
attachment.class.Attachment,
|
attachment.class.Attachment,
|
||||||
message.space,
|
message.space,
|
||||||
@ -65,9 +66,10 @@ export class AttachmentHandler {
|
|||||||
if (attachment.data == null) return []
|
if (attachment.data == null) return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
file: attachment.data,
|
id: randomUUID(),
|
||||||
name: part.filename,
|
name: part.filename,
|
||||||
type: part.mimeType ?? undefined,
|
data: Buffer.from(attachment.data, 'base64'),
|
||||||
|
contentType: part.mimeType ?? 'application/octet-stream',
|
||||||
size: attachment.size ?? undefined,
|
size: attachment.size ?? undefined,
|
||||||
lastModified: new Date().getTime()
|
lastModified: new Date().getTime()
|
||||||
}
|
}
|
||||||
@ -76,9 +78,10 @@ export class AttachmentHandler {
|
|||||||
if (part.body?.data == null) return []
|
if (part.body?.data == null) return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
file: part.body.data,
|
id: randomUUID(),
|
||||||
|
data: Buffer.from(part.body.data, 'base64'),
|
||||||
name: part.filename,
|
name: part.filename,
|
||||||
type: part.mimeType ?? undefined,
|
contentType: part.mimeType ?? 'application/octet-stream',
|
||||||
size: part.body.size ?? undefined,
|
size: part.body.size ?? undefined,
|
||||||
lastModified: new Date().getTime()
|
lastModified: new Date().getTime()
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@ import { gmail_v1 } from 'googleapis'
|
|||||||
|
|
||||||
import { type MeasureContext, PersonId } from '@hcengineering/core'
|
import { type MeasureContext, PersonId } from '@hcengineering/core'
|
||||||
import { type KeyValueClient } from '@hcengineering/kvs-client'
|
import { type KeyValueClient } from '@hcengineering/kvs-client'
|
||||||
|
import { SyncMutex } from '@hcengineering/mail-common'
|
||||||
|
|
||||||
import { RateLimiter } from '../rateLimiter'
|
import { RateLimiter } from '../rateLimiter'
|
||||||
import { MessageManager } from './message'
|
import { IMessageManager } from './types'
|
||||||
|
|
||||||
interface History {
|
interface History {
|
||||||
historyId: string
|
historyId: string
|
||||||
@ -27,41 +28,12 @@ interface History {
|
|||||||
workspace: string
|
workspace: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyncMutex {
|
|
||||||
private readonly locks = new Map<string, Promise<void>>()
|
|
||||||
|
|
||||||
async lock (key: string): Promise<() => void> {
|
|
||||||
// Wait for any existing lock to be released
|
|
||||||
const currentLock = this.locks.get(key)
|
|
||||||
if (currentLock != null) {
|
|
||||||
await currentLock
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new lock
|
|
||||||
let releaseFn!: () => void
|
|
||||||
const newLock = new Promise<void>((resolve) => {
|
|
||||||
releaseFn = resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store the lock
|
|
||||||
this.locks.set(key, newLock)
|
|
||||||
|
|
||||||
// Return the release function
|
|
||||||
return () => {
|
|
||||||
if (this.locks.get(key) === newLock) {
|
|
||||||
this.locks.delete(key)
|
|
||||||
}
|
|
||||||
releaseFn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SyncManager {
|
export class SyncManager {
|
||||||
private readonly syncMutex = new SyncMutex()
|
private readonly syncMutex = new SyncMutex()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly ctx: MeasureContext,
|
private readonly ctx: MeasureContext,
|
||||||
private readonly messageManager: MessageManager,
|
private readonly messageManager: IMessageManager,
|
||||||
private readonly gmail: gmail_v1.Resource$Users,
|
private readonly gmail: gmail_v1.Resource$Users,
|
||||||
private readonly workspace: string,
|
private readonly workspace: string,
|
||||||
private readonly keyValueClient: KeyValueClient,
|
private readonly keyValueClient: KeyValueClient,
|
||||||
|
36
services/gmail/pod-gmail/src/message/types.ts
Normal file
36
services/gmail/pod-gmail/src/message/types.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// 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 { type GaxiosResponse } from 'gaxios'
|
||||||
|
import { gmail_v1 } from 'googleapis'
|
||||||
|
|
||||||
|
export interface AttachedFile {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
data: Buffer
|
||||||
|
contentType: string
|
||||||
|
size?: number
|
||||||
|
lastModified: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailContact {
|
||||||
|
email: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
photoUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMessageManager {
|
||||||
|
saveMessage: (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string) => Promise<void>
|
||||||
|
}
|
31
services/gmail/pod-gmail/src/message/utils.ts
Normal file
31
services/gmail/pod-gmail/src/message/utils.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// 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 TurndownService from 'turndown'
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
import { EmailMessage } from '@hcengineering/mail-common'
|
||||||
|
import { MeasureContext } from '@hcengineering/core'
|
||||||
|
|
||||||
|
export function getMdContent (ctx: MeasureContext, email: EmailMessage): string {
|
||||||
|
if (email.content !== undefined) {
|
||||||
|
try {
|
||||||
|
const html = sanitizeHtml(email.content)
|
||||||
|
const tds = new TurndownService()
|
||||||
|
return tds.turndown(html)
|
||||||
|
} catch (error) {
|
||||||
|
ctx.warn('Failed to parse html content', { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return email.textContent
|
||||||
|
}
|
@ -32,15 +32,16 @@ import core from '@hcengineering/core'
|
|||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
import { type Channel } from '../types'
|
import { IMessageManager } from '../types'
|
||||||
import { AttachmentHandler } from './attachments'
|
import { type Channel } from '../../types'
|
||||||
import { decode64 } from '../base64'
|
import { AttachmentHandler } from '../attachments'
|
||||||
import { diffAttributes } from '../utils'
|
import { decode64 } from '../../base64'
|
||||||
|
import { diffAttributes } from '../../utils'
|
||||||
|
|
||||||
const EMAIL_REGEX =
|
const EMAIL_REGEX =
|
||||||
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
|
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
|
||||||
|
|
||||||
export class MessageManager {
|
export class MessageManagerV1 implements IMessageManager {
|
||||||
constructor (
|
constructor (
|
||||||
private readonly ctx: MeasureContext,
|
private readonly ctx: MeasureContext,
|
||||||
private readonly client: TxOperations,
|
private readonly client: TxOperations,
|
103
services/gmail/pod-gmail/src/message/v2/message.ts
Normal file
103
services/gmail/pod-gmail/src/message/v2/message.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// 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 { type GaxiosResponse } from 'gaxios'
|
||||||
|
import { gmail_v1 } from 'googleapis'
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
import { SocialId, type MeasureContext } from '@hcengineering/core'
|
||||||
|
import { createMessages, parseNameFromEmailHeader, EmailMessage } from '@hcengineering/mail-common'
|
||||||
|
|
||||||
|
import { IMessageManager } from '../types'
|
||||||
|
import config from '../../config'
|
||||||
|
import { AttachmentHandler } from '../attachments'
|
||||||
|
import { decode64 } from '../../base64'
|
||||||
|
|
||||||
|
export class MessageManagerV2 implements IMessageManager {
|
||||||
|
constructor (
|
||||||
|
private readonly ctx: MeasureContext,
|
||||||
|
private readonly attachmentHandler: AttachmentHandler,
|
||||||
|
private readonly token: string,
|
||||||
|
private readonly socialId: SocialId
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async saveMessage (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string): Promise<void> {
|
||||||
|
const res = convertMessage(message, me)
|
||||||
|
const attachments = await this.attachmentHandler.getPartFiles(message.data.payload, message.data.id ?? '')
|
||||||
|
|
||||||
|
await createMessages(config, this.ctx, this.token, res, attachments, me, this.socialId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeaderValue (payload: gmail_v1.Schema$MessagePart | undefined, name: string): string | undefined {
|
||||||
|
if (payload === undefined) return undefined
|
||||||
|
const headers = payload.headers
|
||||||
|
|
||||||
|
return headers?.find((header) => header.name?.toLowerCase() === name.toLowerCase())?.value ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartsMessage (parts: gmail_v1.Schema$MessagePart[] | undefined, mime: string): string {
|
||||||
|
let result = ''
|
||||||
|
if (parts !== undefined) {
|
||||||
|
const htmlPart = parts.find((part) => part.mimeType === mime)
|
||||||
|
const filtredParts = htmlPart !== undefined ? parts.filter((part) => part.mimeType === mime) : parts
|
||||||
|
for (const part of filtredParts ?? []) {
|
||||||
|
result += getPartMessage(part, mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeOptions: sanitizeHtml.IOptions = {
|
||||||
|
allowedTags: [],
|
||||||
|
allowedAttributes: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeText (input: string): string {
|
||||||
|
if (input == null) return ''
|
||||||
|
return sanitizeHtml(input, sanitizeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartMessage (part: gmail_v1.Schema$MessagePart | undefined, mime: string): string {
|
||||||
|
if (part === undefined) return ''
|
||||||
|
if (part.body?.data != null) {
|
||||||
|
return decode64(part.body.data)
|
||||||
|
}
|
||||||
|
return getPartsMessage(part.parts, mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertMessage (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string): EmailMessage {
|
||||||
|
const date = message.data.internalDate != null ? new Date(Number.parseInt(message.data.internalDate)) : new Date()
|
||||||
|
const from = parseNameFromEmailHeader(getHeaderValue(message.data.payload, 'From') ?? '')
|
||||||
|
const to = parseNameFromEmailHeader(getHeaderValue(message.data.payload, 'To') ?? '')
|
||||||
|
|
||||||
|
const copy =
|
||||||
|
getHeaderValue(message.data.payload, 'Cc')
|
||||||
|
?.split(',')
|
||||||
|
.map((p) => parseNameFromEmailHeader(p.trim())) ?? undefined
|
||||||
|
const incoming = !from.email.includes(me)
|
||||||
|
return {
|
||||||
|
modifiedOn: date.getTime(),
|
||||||
|
mailId: getHeaderValue(message.data.payload, 'Message-ID') ?? '',
|
||||||
|
replyTo: getHeaderValue(message.data.payload, 'In-Reply-To'),
|
||||||
|
copy,
|
||||||
|
content: sanitizeHtml(getPartMessage(message.data.payload, 'text/html')),
|
||||||
|
textContent: sanitizeText(getPartMessage(message.data.payload, 'text/plain')),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
incoming,
|
||||||
|
subject: getHeaderValue(message.data.payload, 'Subject') ?? '',
|
||||||
|
sendOn: date.getTime()
|
||||||
|
}
|
||||||
|
}
|
@ -37,14 +37,6 @@ export type State = User & {
|
|||||||
redirectURL: string
|
redirectURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttachedFile {
|
|
||||||
size?: number
|
|
||||||
file: string
|
|
||||||
type?: string
|
|
||||||
lastModified: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Channel = Pick<PlatformChannel, 'value' | keyof Doc>
|
export type Channel = Pick<PlatformChannel, 'value' | keyof Doc>
|
||||||
|
|
||||||
export type RequestType = 'get' | 'post'
|
export type RequestType = 'get' | 'post'
|
||||||
|
Loading…
Reference in New Issue
Block a user