Skip to content

Commit 53f5c46

Browse files
authored
mine from PST & OST files (#2564)
* install pst-extractor * mine from pst * remove "user email" from pst minings * use supabase storage * create a global prettier:fix * handle mining completion * prettier * add indeterminate progress bar * publishFetchingProgress and stop on mining finish * Update 20251218184211_create_pst_bucket.sql 5GB maximum file * add file upload progress bar and start next step * mining type; pst * getTotalMessages & await processFolder * Pre-scan PST to compute total messages before processing * Update PSTEmailsFetcher.ts * createSignedUploadUrl * i18n & typing fixes * delete file after mining * create scheduled job to remove PST files * linting fixes * linting fixes * linting fixes
1 parent 5af0198 commit 53f5c46

File tree

27 files changed

+1787
-81
lines changed

27 files changed

+1787
-81
lines changed

backend/package-lock.json

Lines changed: 36 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"pg": "^8.16.3",
6161
"pg-format": "^1.0.4",
6262
"planer": "^1.2.0",
63+
"pst-extractor": "^1.11.0",
6364
"qs": "^6.14.0",
6465
"quoted-printable": "^1.0.1",
6566
"rate-limiter-flexible": "^9.0.0",

backend/src/app.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import initializeStreamRouter from './routes/stream.routes';
2222
import AuthResolver from './services/auth/AuthResolver';
2323
import TasksManagerFile from './services/tasks-manager/TaskManagerFile';
2424
import TasksManager from './services/tasks-manager/TasksManager';
25+
import TasksManagerPST from './services/tasks-manager/TasksManagerPST';
2526
import Billing from './utils/billing-plugin';
2627

2728
export default function initializeApp(
2829
authResolver: AuthResolver,
2930
tasksManager: TasksManager,
3031
tasksManagerFile: TasksManagerFile,
32+
tasksManagerPST: TasksManagerPST,
3133
miningSources: MiningSources,
3234
contacts: Contacts,
3335
userResolver: Users,
@@ -62,13 +64,19 @@ export default function initializeApp(
6264
app.use('/api/imap', initializeImapRoutes(authResolver, miningSources));
6365
app.use(
6466
'/api/imap',
65-
initializeStreamRouter(tasksManager, tasksManagerFile, authResolver)
67+
initializeStreamRouter(
68+
tasksManager,
69+
tasksManagerFile,
70+
tasksManagerPST,
71+
authResolver
72+
)
6673
);
6774
app.use(
6875
'/api/imap',
6976
initializeMiningRoutes(
7077
tasksManager,
7178
tasksManagerFile,
79+
tasksManagerPST,
7280
miningSources,
7381
authResolver
7482
)

backend/src/controllers/mining.controller.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import {
99
import { ContactFormat } from '../services/extractors/engines/FileImport';
1010
import TaskManagerFile from '../services/tasks-manager/TaskManagerFile';
1111
import TasksManager from '../services/tasks-manager/TasksManager';
12+
import TasksManagerPST from '../services/tasks-manager/TasksManagerPST';
13+
import { Task } from '../services/tasks-manager/types';
1214
import { ImapAuthError } from '../utils/errors';
1315
import validateType from '../utils/helpers/validation';
1416
import logger from '../utils/logger';
1517
import redis from '../utils/redis';
18+
import supabaseClient from '../utils/supabase';
1619
import {
1720
generateErrorObjectFromImapError,
1821
getValidImapLogin,
@@ -24,8 +27,6 @@ import {
2427
getTokenWithScopeValidation,
2528
validateFileContactsData
2629
} from './mining.helpers';
27-
import supabaseClient from '../utils/supabase';
28-
import { Task } from '../services/tasks-manager/types';
2930

3031
/**
3132
* Exchanges an OAuth authorization code for tokens and extracts user email
@@ -56,6 +57,7 @@ async function exchangeForToken(
5657
export default function initializeMiningController(
5758
tasksManager: TasksManager,
5859
tasksManagerFile: TaskManagerFile,
60+
tasksManagerPST: TasksManagerPST,
5961
miningSources: MiningSources
6062
) {
6163
return {
@@ -340,6 +342,55 @@ export default function initializeMiningController(
340342
}
341343
},
342344

345+
async startMiningPST(req: Request, res: Response, next: NextFunction) {
346+
const user = res.locals.user as User;
347+
348+
const {
349+
name,
350+
extractSignatures
351+
}: {
352+
name: string;
353+
extractSignatures: boolean;
354+
// file
355+
} = req.body;
356+
357+
const errors = [
358+
validateType('name', name, 'string'),
359+
validateType('extractSignatures', extractSignatures, 'boolean')
360+
].filter(Boolean);
361+
362+
if (errors.length) {
363+
return res
364+
.status(400)
365+
.json({ message: `Invalid input: ${errors.join(', ')}` });
366+
}
367+
try {
368+
const miningTask = await tasksManagerPST.createTask(
369+
user.id,
370+
name,
371+
extractSignatures
372+
);
373+
return res.status(201).send({ error: null, data: miningTask });
374+
} catch (err) {
375+
if (
376+
err instanceof Error &&
377+
err.message.toLowerCase().startsWith('invalid credentials')
378+
) {
379+
return res.status(401).json({ message: err.message });
380+
}
381+
if (
382+
err instanceof Error &&
383+
'textCode' in err &&
384+
err.textCode === 'CANNOT'
385+
) {
386+
return res.sendStatus(409);
387+
}
388+
389+
res.status(500);
390+
return next(err);
391+
}
392+
},
393+
343394
async stopMiningTask(req: Request, res: Response, next: NextFunction) {
344395
const { type: miningType } = req.params;
345396
const { user } = res.locals;
@@ -349,7 +400,14 @@ export default function initializeMiningController(
349400
return next(new Error('user does not exists.'));
350401
}
351402

352-
const manager = miningType === 'file' ? tasksManagerFile : tasksManager;
403+
let manager;
404+
if (miningType === 'file') {
405+
manager = tasksManagerFile;
406+
} else if (miningType === 'pst') {
407+
manager = tasksManagerPST;
408+
} else {
409+
manager = tasksManager;
410+
}
353411

354412
const { id: taskId } = req.params;
355413
const {
@@ -452,6 +510,11 @@ export default function initializeMiningController(
452510
(!extractTask || !cleanTask)
453511
) {
454512
throw new Error(`File mining with id: ${miningId} not found`);
513+
} else if (
514+
task.miningSource.type === 'pst' &&
515+
(!extractTask || !cleanTask)
516+
) {
517+
throw new Error(`PST mining with id: ${miningId} not found`);
455518
}
456519

457520
if (user.id !== task.userId) {

backend/src/controllers/stream.controller.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { Request, Response } from 'express';
2+
import TasksManagerFile from '../services/tasks-manager/TaskManagerFile';
23
import TasksManager from '../services/tasks-manager/TasksManager';
4+
import TasksManagerPST from '../services/tasks-manager/TasksManagerPST';
35
import logger from '../utils/logger';
4-
import TasksManagerFile from '../services/tasks-manager/TaskManagerFile';
56

67
export default function initializeStreamController(
78
tasksManager: TasksManager,
8-
tasksManagerFile: TasksManagerFile
9+
tasksManagerFile: TasksManagerFile,
10+
tasksManagerPST: TasksManagerPST
911
) {
1012
return {
1113
/**
1214
* Stream the progress of email extraction and scanning via Server-Sent Events (SSE).
1315
*/
1416
streamProgress: (req: Request, res: Response) => {
1517
const { id: taskId, type: miningType } = req.params;
16-
const manager = miningType === 'file' ? tasksManagerFile : tasksManager;
18+
19+
let manager;
20+
if (miningType === 'file') {
21+
manager = tasksManagerFile;
22+
} else if (miningType === 'pst') {
23+
manager = tasksManagerPST;
24+
} else {
25+
manager = tasksManager;
26+
}
1727

1828
try {
1929
const task = manager.getActiveTask(taskId);

backend/src/routes/mining.routes.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import initializeMiningController from '../controllers/mining.controller';
33
import { MiningSources } from '../db/interfaces/MiningSources';
44
import initializeAuthMiddleware from '../middleware/auth';
55
import AuthResolver from '../services/auth/AuthResolver';
6-
import TasksManager from '../services/tasks-manager/TasksManager';
76
import TasksManagerFile from '../services/tasks-manager/TaskManagerFile';
7+
import TasksManager from '../services/tasks-manager/TasksManager';
8+
import TasksManagerPST from '../services/tasks-manager/TasksManagerPST';
89

910
export default function initializeMiningRoutes(
1011
tasksManager: TasksManager,
1112
tasksManagerFile: TasksManagerFile,
13+
tasksManagerPST: TasksManagerPST,
1214
miningSource: MiningSources,
1315
authResolver: AuthResolver
1416
) {
@@ -17,13 +19,19 @@ export default function initializeMiningRoutes(
1719
const {
1820
startMining,
1921
startMiningFile,
22+
startMiningPST,
2023
stopMiningTask,
2124
getMiningTask,
2225
createProviderMiningSource,
2326
createProviderMiningSourceCallback,
2427
createImapMiningSource,
2528
getMiningSources
26-
} = initializeMiningController(tasksManager, tasksManagerFile, miningSource);
29+
} = initializeMiningController(
30+
tasksManager,
31+
tasksManagerFile,
32+
tasksManagerPST,
33+
miningSource
34+
);
2735

2836
const authMiddleware = initializeAuthMiddleware(authResolver);
2937

@@ -44,6 +52,7 @@ export default function initializeMiningRoutes(
4452
router.get('/mine/:userId/', authMiddleware, getMiningTask);
4553
router.post('/mine/email/:userId', authMiddleware, startMining);
4654
router.post('/mine/file/:userId', authMiddleware, startMiningFile);
55+
router.post('/mine/pst/:userId', authMiddleware, startMiningPST);
4756
router.post('/mine/:type/:userId/:id', authMiddleware, stopMiningTask);
4857

4958
return router;

backend/src/routes/stream.routes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { Router } from 'express';
22
import initializeStreamController from '../controllers/stream.controller';
33
import initializeAuthMiddleware from '../middleware/auth';
44
import AuthResolver from '../services/auth/AuthResolver';
5-
import TasksManager from '../services/tasks-manager/TasksManager';
65
import TasksManagerFile from '../services/tasks-manager/TaskManagerFile';
6+
import TasksManager from '../services/tasks-manager/TasksManager';
7+
import TasksManagerPST from '../services/tasks-manager/TasksManagerPST';
78

89
export default function initializeStreamRouter(
910
tasksManager: TasksManager,
1011
tasksManagerFile: TasksManagerFile,
12+
tasksManagerPST: TasksManagerPST,
1113
authResolver: AuthResolver
1214
) {
1315
const router = Router();
@@ -16,7 +18,8 @@ export default function initializeStreamRouter(
1618

1719
const { streamProgress } = initializeStreamController(
1820
tasksManager,
19-
tasksManagerFile
21+
tasksManagerFile,
22+
tasksManagerPST
2023
);
2124
router.get('/mine/:type/:id/progress/', authMiddleware, streamProgress);
2225

backend/src/server.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import ENV from './config';
55
import pool from './db/pg';
66
import PgContacts from './db/pg/PgContacts';
77
import PgMiningSources from './db/pg/PgMiningSources';
8+
import SupabaseTasks from './db/supabase/tasks';
89
import SupabaseUsers from './db/supabase/users';
910
import SupabaseAuthResolver from './services/auth/SupabaseAuthResolver';
11+
import EmailFetcherClient from './services/email-fetching';
12+
import PSTFetcherClient from './services/email-fetching/pst';
1013
import SSEBroadcasterFactory from './services/factory/SSEBroadcasterFactory';
14+
import TasksManagerFile from './services/tasks-manager/TaskManagerFile';
1115
import TasksManager from './services/tasks-manager/TasksManager';
16+
import TasksManagerPST from './services/tasks-manager/TasksManagerPST';
1217
import { flickrBase58IdGenerator } from './services/tasks-manager/utils';
1318
import logger from './utils/logger';
1419
import redis from './utils/redis';
1520
import supabaseClient from './utils/supabase';
16-
import SupabaseTasks from './db/supabase/tasks';
17-
import TasksManagerFile from './services/tasks-manager/TaskManagerFile';
18-
import EmailFetcherClient from './services/email-fetching';
1921

2022
// eslint-disable-next-line no-console
2123
console.log(
@@ -63,10 +65,24 @@ console.log(
6365
new SSEBroadcasterFactory(),
6466
flickrBase58IdGenerator()
6567
);
68+
const tasksManagerPST = new TasksManagerPST(
69+
tasksResolver,
70+
redis.getSubscriberClient(),
71+
redis.getClient(),
72+
new PSTFetcherClient(
73+
logger,
74+
ENV.EMAIL_FETCHING_SERVICE_API_TOKEN, //! RDNDNT
75+
ENV.EMAIL_FETCHING_SERVICE_URL
76+
),
77+
new SSEBroadcasterFactory(),
78+
flickrBase58IdGenerator()
79+
);
80+
6681
const app = initializeApp(
6782
authResolver,
6883
tasksManager,
6984
tasksManagerFile,
85+
tasksManagerPST,
7086
miningSources,
7187
contactsResolver,
7288
userResolver,

0 commit comments

Comments
 (0)