Skip to content

Commit

Permalink
[FIX] Correct IMAP configuration for email inbox (#25789)
Browse files Browse the repository at this point in the history
Co-authored-by: Murtaza Patrawala <[email protected]>
Co-authored-by: Kevin Aleman <[email protected]>
  • Loading branch information
3 people authored Aug 25, 2022
1 parent 063de7d commit 1de809a
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 53 deletions.
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,
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 @@ -3090,6 +3090,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 @@ -2869,6 +2869,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: 53 additions & 29 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);
this.retries = 0;
this.openInbox((err) => {
if (err) {
logger.error(`Error occurred during imap on inbox ${this.config.user}: `, err);
throw err;
}
// 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,37 @@ 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 ....');
reconnect(): void {
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 +142,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 +164,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),
...(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

0 comments on commit 1de809a

Please sign in to comment.