Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Correct IMAP configuration for email inbox #25789

Merged
merged 58 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
63d1dd5
[FIX] the previous tag, UNSEEN, was no longer retrieving, but still m…
cauefcr Jun 2, 2022
3606333
well, turns out the forceNoop was also needed
cauefcr Jun 2, 2022
08cd7a3
last changes before gmail blocked me
cauefcr Jun 7, 2022
701b791
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jun 14, 2022
bd78313
backoff reconection
cauefcr Jun 16, 2022
792fcdd
removing debug prints
cauefcr Jun 16, 2022
19a8293
add capability and configuration for maximum retries on imap
cauefcr Jun 20, 2022
1f4c5d0
Update apps/meteor/server/email/IMAPInterceptor.ts
cauefcr Jun 21, 2022
be3dd88
Chore: Avoid using console logs for email feature (#25941)
murtaza98 Jun 21, 2022
4de2cf8
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Jun 22, 2022
b4780f7
Fix typings
KevLehman Jun 22, 2022
7f2fe3c
Change input to be number
KevLehman Jun 22, 2022
1f9c1ff
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Jun 27, 2022
31b3ad4
maxretries not being saved, department not working correctly
KevLehman Jun 27, 2022
2dd40ec
Fix broken matching behavior on department selection
cauefcr Jun 27, 2022
30118b6
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jun 27, 2022
66e2d3b
Merge branch 'fix/imap-breaking-configuration-change' of ssh://github…
cauefcr Jun 27, 2022
d435113
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jun 30, 2022
2d2a3c3
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 4, 2022
e9e9ff1
only define variables when using TLS
cauefcr Jul 4, 2022
06a2cb0
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 7, 2022
bd8728a
Merge branch 'develop' into fix/imap-breaking-configuration-change
cauefcr Jul 13, 2022
dbb36b5
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 14, 2022
028e05c
even more retry attempts, and proper giving up
cauefcr Jul 14, 2022
7d6366e
Merge branch 'fix/imap-breaking-configuration-change' of ssh://github…
cauefcr Jul 14, 2022
478d38d
Merge branch 'develop' into fix/imap-breaking-configuration-change
cauefcr Jul 14, 2022
1e59da1
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 21, 2022
d7439bf
room unhold on new e-mail and better error messages
cauefcr Jul 21, 2022
2f88c3e
more robust thread query
cauefcr Jul 21, 2022
d87fb87
linting
cauefcr Jul 21, 2022
0c60be3
linting
cauefcr Jul 21, 2022
664d52f
Merge branch 'fix/imap-breaking-configuration-change' of ssh://github…
cauefcr Jul 21, 2022
627f530
EE remove from hold
cauefcr Jul 21, 2022
5d75a17
massaging typescript
cauefcr Jul 22, 2022
899da15
adding token as the thread for the email, so it better emulates a use…
cauefcr Jul 25, 2022
0b04450
Update apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
cauefcr Jul 25, 2022
5ad4784
faster timeout for better latency, and making sure changes propagate
cauefcr Jul 26, 2022
2a9cd3c
Merge branch 'develop' into fix/imap-breaking-configuration-change
murtaza98 Jul 28, 2022
c425d3b
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 1, 2022
c4a26ea
add some tests
cauefcr Aug 1, 2022
d744288
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 1, 2022
c4f8ab3
Merge branch 'develop' into fix/imap-breaking-configuration-change
cauefcr Aug 2, 2022
2cb4208
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Aug 15, 2022
789f001
extra logs
KevLehman Aug 15, 2022
088724d
fix lint
KevLehman Aug 15, 2022
41b5559
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Aug 18, 2022
fdc3db1
Threads work properly now, e-mail replies should remain in the same r…
cauefcr Aug 18, 2022
dc2fe58
Merge branch 'fix/imap-breaking-configuration-change' of ssh://github…
cauefcr Aug 18, 2022
50e2690
Merge branch 'develop' into fix/imap-breaking-configuration-change
cauefcr Aug 18, 2022
ce703f3
soft-upgrading rather than hard breaking existing conversations
cauefcr Aug 18, 2022
05bf319
Merge branch 'fix/imap-breaking-configuration-change' of ssh://github…
cauefcr Aug 18, 2022
a65ff74
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 18, 2022
323d0e9
watchOnce again the livechat_routing_method
cauefcr Aug 18, 2022
af61068
old mongo has no $regex, and a bug fix from old commit
cauefcr Aug 18, 2022
a20d51b
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Aug 19, 2022
362aaa4
Merge branch 'develop' into fix/imap-breaking-configuration-change
KevLehman Aug 19, 2022
77f724c
infinite, fast retries error
cauefcr Aug 22, 2022
781d942
Merge branch 'develop' into fix/imap-breaking-configuration-change
kodiakhq[bot] Aug 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/meteor/app/api/server/v1/email-inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ API.v1.addRoute(
if (!hasPermission(this.userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}

check(this.bodyParams, {
_id: Match.Maybe(String),
active: Boolean,
Expand All @@ -50,6 +51,7 @@ API.v1.addRoute(
username: String,
password: String,
secure: Boolean,
maxRetries: Number,
}),
});

Expand Down Expand Up @@ -126,6 +128,7 @@ API.v1.addRoute(
const { email } = this.queryParams;

// TODO: Chapter day backend - check if user has permission to view this email inbox instead of null values
// TODO: Chapter day: Remove this endpoint and move search to GET /email-inbox
const emailInbox = await EmailInbox.findOne({ email });

return API.v1.success({ emailInbox });
Expand Down
16 changes: 13 additions & 3 deletions apps/meteor/app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export class LivechatRooms extends Base {
const query = {
't': 'l',
'v.token': visitorToken,
'email.thread': emailThread,
'$or': [{ 'email.thread': { $elemMatch: { $in: emailThread } } }, { 'email.thread': new RegExp(emailThread.join('|')) }],
};

return this.findOne(query, options);
Expand All @@ -208,7 +208,7 @@ export class LivechatRooms extends Base {
const query = {
't': 'l',
'v.token': visitorToken,
'email.thread': emailThread,
'$or': [{ 'email.thread': { $elemMatch: { $in: emailThread } } }, { 'email.thread': new RegExp(emailThread.join('|')) }],
...(departmentId && { departmentId }),
};

Expand All @@ -220,12 +220,22 @@ export class LivechatRooms extends Base {
't': 'l',
'open': true,
'v.token': visitorToken,
'email.thread': emailThread,
'$or': [{ 'email.thread': { $elemMatch: { $in: emailThread } } }, { 'email.thread': new RegExp(emailThread.join('|')) }],
};

return this.findOne(query, options);
}

updateEmailThreadByRoomId(roomId, threadIds) {
const query = {
$addToSet: {
'email.thread': threadIds,
},
};

return this.update({ _id: roomId }, query);
}

findOneLastServedAndClosedByVisitorToken(visitorToken, options = {}) {
const query = {
't': 'l',
Expand Down
15 changes: 13 additions & 2 deletions apps/meteor/client/views/admin/emailInbox/EmailInboxForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const initialValues = {
imapUsername: '',
imapPassword: '',
imapSecure: false,
imapRetries: 10,
};

const getInitialValues = (data) => {
Expand Down Expand Up @@ -68,6 +69,7 @@ const getInitialValues = (data) => {
imapUsername: imap.username ?? '',
imapPassword: imap.password ?? '',
imapSecure: imap.secure ?? false,
imapRetries: imap.maxRetries ?? 10,
};
};

Expand Down Expand Up @@ -96,6 +98,7 @@ function EmailInboxForm({ id, data }) {
handleImapPort,
handleImapUsername,
handleImapPassword,
handleImapRetries,
handleImapSecure,
} = handlers;
const {
Expand All @@ -116,6 +119,7 @@ function EmailInboxForm({ id, data }) {
imapPort,
imapUsername,
imapPassword,
imapRetries,
imapSecure,
} = values;

Expand Down Expand Up @@ -174,15 +178,16 @@ function EmailInboxForm({ id, data }) {
username: imapUsername,
password: imapPassword,
secure: imapSecure,
maxRetries: parseInt(imapRetries),
};
const departmentValue = department.value;

const payload = {
active,
name,
email,
description,
senderInfo,
department: departmentValue,
department: typeof department === 'string' ? department : department.value,
cauefcr marked this conversation as resolved.
Show resolved Hide resolved
smtp,
imap,
};
Expand Down Expand Up @@ -331,6 +336,12 @@ function EmailInboxForm({ id, data }) {
<TextInput type='password' value={imapPassword} onChange={handleImapPassword} />
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Max_Retry')}*</Field.Label>
<Field.Row>
<TextInput type='number' value={imapRetries} onChange={handleImapRetries} />
</Field.Row>
</Field>
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Connect_SSL_TLS')}
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3085,6 +3085,7 @@
"Max_number_of_chats_per_agent": "Max. number of simultaneous chats",
"Max_number_of_chats_per_agent_description": "The max. number of simultaneous chats that the agents can attend",
"Max_number_of_uses": "Max number of uses",
"Max_Retry": "Maximum attemps to reconnect to the server",
"Maximum": "Maximum",
"Maximum_number_of_guests_reached": "Maximum number of guests reached",
"Me": "Me",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2888,6 +2888,7 @@
"Max_number_of_chats_per_agent": "Número máximo de conversas simultâneas",
"Max_number_of_chats_per_agent_description": "Número máximo de conversas simultâneas de que um agente pode participar",
"Max_number_of_uses": "Número máximo de usos",
"Max_Retry": "Número máximo de tentativas de conexão com o servidor",
"Maximum": "Máximo",
"Maximum_number_of_guests_reached": "Número máximo de visitantes atingido",
"Me": "Eu",
Expand Down
82 changes: 57 additions & 25 deletions apps/meteor/server/email/IMAPInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import type Connection from 'imap';
import type { ParsedMail } from 'mailparser';
import { simpleParser } from 'mailparser';

import { logger } from '../features/EmailInbox/logger';

type IMAPOptions = {
deleteAfterRead: boolean;
filter: any[];
rejectBeforeTS?: Date;
markSeen: boolean;
maxRetries: number;
};

export declare interface IMAPInterceptor {
Expand All @@ -19,30 +22,42 @@ export declare interface IMAPInterceptor {
export class IMAPInterceptor extends EventEmitter {
private imap: IMAP;

private options: IMAPOptions;
private config: IMAP.Config;

private initialBackoffDurationMS = 30000;

private backoff: NodeJS.Timeout;

constructor(imapConfig: IMAP.Config, options?: Partial<IMAPOptions>) {
private retries = 0;

constructor(
imapConfig: IMAP.Config,
private options: IMAPOptions = {
deleteAfterRead: false,
filter: ['UNSEEN'],
markSeen: true,
maxRetries: 10,
},
) {
super();

this.config = imapConfig;

this.imap = new IMAP({
connTimeout: 30000,
connTimeout: 10000,
keepalive: true,
...(imapConfig.tls && { tlsOptions: { servername: imapConfig.host } }),
...imapConfig,
});

this.options = {
deleteAfterRead: false,
filter: ['UNSEEN'],
markSeen: true,
...options,
};

// On successfully connected.
this.imap.on('ready', () => {
if (this.imap.state !== 'disconnected') {
clearTimeout(this.backoff);
cauefcr marked this conversation as resolved.
Show resolved Hide resolved
this.retries = 0;
this.openInbox((err) => {
if (err) {
logger.error(`Error occurred during imap on inbox ${this.config.user}: `, err);
throw err;
cauefcr marked this conversation as resolved.
Show resolved Hide resolved
}
// fetch new emails & wait [IDLE]
Expand All @@ -54,19 +69,20 @@ export class IMAPInterceptor extends EventEmitter {
});
});
} else {
this.log('IMAP did not connected.');
logger.error(`IMAP did not connect on inbox ${this.config.user}`);
this.imap.end();
this.reconnect();
}
});

this.imap.on('error', (err: Error) => {
this.log('Error occurred ...', err);
throw err;
logger.error(`Error occurred on inbox ${this.config.user}: `, err);
this.stop(() => this.reconnect());
});
}

log(...msg: any[]): void {
console.log(...msg);
this.imap.on('close', () => {
this.reconnect();
});
}

openInbox(cb: (error: Error, mailbox: Connection.Box) => void): void {
Expand All @@ -86,30 +102,45 @@ export class IMAPInterceptor extends EventEmitter {
}

stop(callback = new Function()): void {
this.log('IMAP stop called');
logger.info('IMAP stop called');
this.imap.end();
this.imap.once('end', () => {
this.log('IMAP stopped');
logger.info('IMAP stopped');
callback?.();
});
callback?.();
}

restart(): void {
this.stop(() => {
this.log('Restarting IMAP ....');
logger.info('Restarting IMAP ....');
this.start();
});
}

reconnect(): void {
cauefcr marked this conversation as resolved.
Show resolved Hide resolved
const loop = (): void => {
this.start();
if (this.retries < this.options.maxRetries) {
this.retries += 1;
this.initialBackoffDurationMS *= 2;
this.backoff = setTimeout(loop, this.initialBackoffDurationMS);
} else {
logger.error(`IMAP reconnection failed on inbox ${this.config.user}`);
clearTimeout(this.backoff);
}
};
this.backoff = setTimeout(loop, this.initialBackoffDurationMS);
}

// Fetch all UNSEEN messages and pass them for further processing
getEmails(): void {
this.imap.search(this.options.filter, (err, newEmails) => {
logger.debug(`IMAP search on inbox ${this.config.user} returned ${newEmails.length} new emails: `, newEmails);
if (err) {
this.log(err);
logger.error(err);
throw err;
}

// newEmails => array containing serials of unseen messages
if (newEmails.length > 0) {
const fetch = this.imap.fetch(newEmails, {
Expand All @@ -119,17 +150,18 @@ export class IMAPInterceptor extends EventEmitter {
});

fetch.on('message', (msg, seqno) => {
logger.debug('E-mail received', seqno, msg);

msg.on('body', (stream, type) => {
if (type.which !== '') {
return;
}

simpleParser(stream, (_err, email) => {
if (this.options.rejectBeforeTS && email.date && email.date < this.options.rejectBeforeTS) {
this.log('Rejecting email', email.subject);
logger.error(`Rejecting email on inbox ${this.config.user}`, email.subject);
return;
}

this.emit('email', email);
});
});
Expand All @@ -140,15 +172,15 @@ export class IMAPInterceptor extends EventEmitter {
if (this.options.deleteAfterRead) {
this.imap.seq.addFlags(seqno, 'Deleted', (err) => {
if (err) {
this.log(`Mark deleted error: ${err}`);
logger.error(`Mark deleted error: ${err}`);
}
});
}
});
});

fetch.once('error', (err) => {
this.log(`Fetch error: ${err}`);
logger.error(`Fetch error: ${err}`);
});
}
});
Expand Down
14 changes: 9 additions & 5 deletions apps/meteor/server/features/EmailInbox/EmailInbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,21 @@ export async function configureEmailInboxes(): Promise<void> {
user: emailInboxRecord.imap.username,
host: emailInboxRecord.imap.server,
port: emailInboxRecord.imap.port,
tls: emailInboxRecord.imap.secure,
tlsOptions: {
rejectUnauthorized: false,
},
// debug: (...args: any[]): void => logger.debug(args),
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
...(emailInboxRecord.imap.secure
? {
tls: emailInboxRecord.imap.secure,
tlsOptions: {
rejectUnauthorized: false,
},
}
: {}),
},
{
deleteAfterRead: false,
filter: [['UNSEEN'], ['SINCE', emailInboxRecord._updatedAt]],
rejectBeforeTS: emailInboxRecord._updatedAt,
markSeen: true,
maxRetries: emailInboxRecord.imap.maxRetries,
},
);

Expand Down
Loading