Skip to content

Commit c2006df

Browse files
authored
V0.4 fix 2 (#210)
* formatting code * Remove uninstalled packages * fix(imap): Improve IMAP connection stability and error handling This commit refactors the IMAP connector to enhance connection management, error handling, and overall stability during email ingestion. The `isConnected` flag has been removed in favor of relying directly on the `client.usable` property from the `imapflow` library. This simplifies the connection logic and avoids state synchronization issues. The `connect` method now re-creates the client instance if it's not usable, ensuring a fresh connection after errors or disconnects. The retry mechanism (`withRetry`) has been updated to no longer manually reset the connection state, as the `connect` method now handles this automatically on the next attempt. Additionally, a minor bug in the `sync-cycle-finished` processor has been fixed. The logic for merging sync states from successful jobs has been simplified and correctly typed, preventing potential runtime errors when no successful jobs are present. --------- Co-authored-by: Wayne <[email protected]>
1 parent 399059a commit c2006df

File tree

4 files changed

+30
-56
lines changed

4 files changed

+30
-56
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Password: openarchiver_demo
5454
- **Thread discovery**: The ability to discover if an email belongs to a thread/conversation and present the context.
5555
- **Compliance & Retention**: Define granular retention policies to automatically manage the lifecycle of your data. Place legal holds on communications to prevent deletion during litigation (TBD).
5656
- **File Hash and Encryption**: Email and attachment file hash values are stored in the meta database upon ingestion, meaning any attempt to alter the file content will be identified, ensuring legal and regulatory compliance.
57+
- - Each archived email comes with an "Integrity Report" feature that indicates if the files are original.
5758
- **Comprehensive Auditing**: An immutable audit trail logs all system activities, ensuring you have a clear record of who accessed what and when.
5859

5960
## 🛠️ Tech Stack

packages/backend/src/jobs/processors/sync-cycle-finished.processor.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ export default async (job: Job<ISyncCycleFinishedJob, any, string>) => {
4949
// if data doesn't have error property, it is a successful job with SyncState
5050
const successfulJobs = allChildJobs.filter((v) => !v || !(v as any).error) as SyncState[];
5151

52-
const finalSyncState =
53-
successfulJobs.length > 0
54-
? deepmerge(...successfulJobs.filter((s) => s && Object.keys(s).length > 0))
55-
: {};
52+
const finalSyncState = deepmerge(
53+
...successfulJobs.filter((s) => s && Object.keys(s).length > 0)
54+
) as SyncState;
5655

5756
const source = await IngestionService.findById(ingestionSourceId);
5857
let status: IngestionStatus = 'active';

packages/backend/src/services/ingestion-connectors/ImapConnector.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { getThreadId } from './helpers/utils';
1515
export class ImapConnector implements IEmailConnector {
1616
private client: ImapFlow;
1717
private newMaxUids: { [mailboxPath: string]: number } = {};
18-
private isConnected = false;
1918
private statusMessage: string | undefined;
2019

2120
constructor(private credentials: GenericImapCredentials) {
@@ -41,7 +40,6 @@ export class ImapConnector implements IEmailConnector {
4140
// Handles client-level errors, like unexpected disconnects, to prevent crashes.
4241
client.on('error', (err) => {
4342
logger.error({ err }, 'IMAP client error');
44-
this.isConnected = false;
4543
});
4644

4745
return client;
@@ -51,20 +49,17 @@ export class ImapConnector implements IEmailConnector {
5149
* Establishes a connection to the IMAP server if not already connected.
5250
*/
5351
private async connect(): Promise<void> {
54-
if (this.isConnected && this.client.usable) {
52+
// If the client is already connected and usable, do nothing.
53+
if (this.client.usable) {
5554
return;
5655
}
5756

58-
// If the client is not usable (e.g., after a logout), create a new one.
59-
if (!this.client.usable) {
60-
this.client = this.createClient();
61-
}
57+
// If the client is not usable (e.g., after a logout or an error), create a new one.
58+
this.client = this.createClient();
6259

6360
try {
6461
await this.client.connect();
65-
this.isConnected = true;
6662
} catch (err: any) {
67-
this.isConnected = false;
6863
logger.error({ err }, 'IMAP connection failed');
6964
if (err.responseText) {
7065
throw new Error(`IMAP Connection Error: ${err.responseText}`);
@@ -77,9 +72,8 @@ export class ImapConnector implements IEmailConnector {
7772
* Disconnects from the IMAP server if the connection is active.
7873
*/
7974
private async disconnect(): Promise<void> {
80-
if (this.isConnected && this.client.usable) {
75+
if (this.client.usable) {
8176
await this.client.logout();
82-
this.isConnected = false;
8377
}
8478
}
8579

@@ -130,8 +124,7 @@ export class ImapConnector implements IEmailConnector {
130124
return await action();
131125
} catch (err: any) {
132126
logger.error({ err, attempt }, `IMAP operation failed on attempt ${attempt}`);
133-
this.isConnected = false; // Force reconnect on next attempt
134-
this.client = this.createClient(); // Create a new client instance for the next retry
127+
// The client is no longer usable, a new one will be created on the next attempt.
135128
if (attempt === maxRetries) {
136129
logger.error({ err }, 'IMAP operation failed after all retries.');
137130
throw err;
@@ -156,6 +149,10 @@ export class ImapConnector implements IEmailConnector {
156149
const mailboxes = await this.withRetry(async () => await this.client.list());
157150

158151
const processableMailboxes = mailboxes.filter((mailbox) => {
152+
// Exclude mailboxes that cannot be selected.
153+
if (mailbox.flags.has('\\Noselect')) {
154+
return false;
155+
}
159156
if (config.app.allInclusiveArchive) {
160157
return true;
161158
}

packages/backend/src/services/ingestion-connectors/MboxConnector.ts

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -98,52 +98,29 @@ export class MboxConnector implements IEmailConnector {
9898
const mboxSplitter = new MboxSplitter();
9999
const emailStream = fileStream.pipe(mboxSplitter);
100100

101-
try {
102-
for await (const emailBuffer of emailStream) {
103-
try {
104-
const emailObject = await this.parseMessage(emailBuffer as Buffer, '');
105-
yield emailObject;
106-
} catch (error) {
107-
logger.error(
108-
{ error, file: this.credentials.uploadedFilePath },
109-
'Failed to process a single message from mbox file. Skipping.'
110-
);
111-
}
112-
}
113-
} finally {
114-
// Ensure all streams are properly closed before deleting the file.
115-
if (fileStream instanceof Readable) {
116-
fileStream.destroy();
117-
}
118-
if (emailStream instanceof Readable) {
119-
emailStream.destroy();
120-
}
121-
// Wait for the streams to fully close to prevent race conditions with file deletion.
122-
await new Promise((resolve) => {
123-
if (fileStream instanceof Readable) {
124-
fileStream.on('close', resolve);
125-
} else {
126-
resolve(true);
127-
}
128-
});
129-
130-
await new Promise((resolve) => {
131-
if (emailStream instanceof Readable) {
132-
emailStream.on('close', resolve);
133-
} else {
134-
resolve(true);
135-
}
136-
});
137-
101+
for await (const emailBuffer of emailStream) {
138102
try {
139-
await this.storage.delete(this.credentials.uploadedFilePath);
103+
const emailObject = await this.parseMessage(emailBuffer as Buffer, '');
104+
yield emailObject;
140105
} catch (error) {
141106
logger.error(
142107
{ error, file: this.credentials.uploadedFilePath },
143-
'Failed to delete mbox file after processing.'
108+
'Failed to process a single message from mbox file. Skipping.'
144109
);
145110
}
146111
}
112+
113+
// After the stream is fully consumed, delete the file.
114+
// The `for await...of` loop ensures streams are properly closed on completion,
115+
// so we can safely delete the file here without causing a hang.
116+
try {
117+
await this.storage.delete(this.credentials.uploadedFilePath);
118+
} catch (error) {
119+
logger.error(
120+
{ error, file: this.credentials.uploadedFilePath },
121+
'Failed to delete mbox file after processing.'
122+
);
123+
}
147124
}
148125

149126
private async parseMessage(emlBuffer: Buffer, path: string): Promise<EmailObject> {

0 commit comments

Comments
 (0)