From 2f90da3f17aa5265023886269bf7b6a6b6cd040a Mon Sep 17 00:00:00 2001
From: Rafael Ferreira <rafaelblink@gmail.com>
Date: Fri, 22 Jan 2021 14:22:55 -0300
Subject: [PATCH] [NEW] Email Inboxes for Omnichannel (#20101)

---
 app/api/server/index.js                       |   1 +
 app/api/server/lib/emailInbox.js              |  79 ++++
 app/api/server/v1/email-inbox.js              | 131 +++++++
 app/authorization/server/startup.js           |   1 +
 .../server/lib/interceptDirectReplyEmails.js  | 133 +------
 .../client/views/app/tabbar/visitorInfo.html  |   2 +
 .../client/views/app/tabbar/visitorInfo.js    |   3 +-
 .../business-hour/AbstractBusinessHour.ts     |   2 +-
 app/livechat/server/lib/QueueManager.js       |  48 ++-
 app/mailer/server/api.js                      |   4 +-
 app/models/server/index.js                    |   2 +
 app/models/server/models/EmailInbox.js        |  27 ++
 app/models/server/models/LivechatRooms.js     |  43 +++
 app/models/server/raw/BaseRaw.ts              |  55 ++-
 app/models/server/raw/EmailInbox.ts           |   6 +
 .../server/raw/LivechatBusinessHours.ts       |  14 -
 app/models/server/raw/index.ts                |   5 +
 .../client/messageBox/messageBox.js           |  21 +-
 .../views/app/lib/getCommonRoomEvents.js      |  25 +-
 app/utils/lib/slashCommand.d.ts               |   3 +
 client/components/AutoCompleteDepartment.js   |   4 +-
 .../Message/Attachments/ActionAttachtment.tsx |   7 +-
 client/components/Sidebar.js                  |   8 +-
 .../views/admin/emailInbox/EmailInboxForm.js  | 361 ++++++++++++++++++
 .../views/admin/emailInbox/EmailInboxPage.js  |  42 ++
 .../views/admin/emailInbox/EmailInboxRoute.js |  17 +
 .../views/admin/emailInbox/EmailInboxTable.js |  73 ++++
 client/views/admin/emailInbox/Skeleton.js     |  11 +
 client/views/admin/routes.js                  |   5 +
 client/views/admin/sidebarItems.js            |   6 +
 .../currentChats/CurrentChatsPage.js          |   2 +-
 definition/IEmailInbox.ts                     |  29 ++
 ee/server/services/stream-hub/StreamHub.ts    |   3 +
 package-lock.json                             | 195 +++++-----
 package.json                                  |   7 +-
 packages/rocketchat-i18n/i18n/en.i18n.json    |  24 +-
 packages/rocketchat-i18n/i18n/pt-BR.i18n.json |  20 +-
 server/email/IMAPInterceptor.ts               | 146 +++++++
 server/features/EmailInbox/EmailInbox.ts      |  69 ++++
 .../EmailInbox/EmailInbox_Incoming.ts         | 203 ++++++++++
 .../EmailInbox/EmailInbox_Outgoing.ts         | 238 ++++++++++++
 server/features/EmailInbox/index.ts           |   2 +
 server/main.js                                |   2 +
 server/modules/watchers/watchers.module.ts    |  18 +
 server/sdk/lib/Events.ts                      |   2 +
 server/services/meteor/service.ts             |   5 +
 46 files changed, 1849 insertions(+), 255 deletions(-)
 create mode 100644 app/api/server/lib/emailInbox.js
 create mode 100644 app/api/server/v1/email-inbox.js
 create mode 100644 app/models/server/models/EmailInbox.js
 create mode 100644 app/models/server/raw/EmailInbox.ts
 create mode 100644 app/utils/lib/slashCommand.d.ts
 create mode 100644 client/views/admin/emailInbox/EmailInboxForm.js
 create mode 100644 client/views/admin/emailInbox/EmailInboxPage.js
 create mode 100644 client/views/admin/emailInbox/EmailInboxRoute.js
 create mode 100644 client/views/admin/emailInbox/EmailInboxTable.js
 create mode 100644 client/views/admin/emailInbox/Skeleton.js
 create mode 100644 definition/IEmailInbox.ts
 create mode 100644 server/email/IMAPInterceptor.ts
 create mode 100644 server/features/EmailInbox/EmailInbox.ts
 create mode 100644 server/features/EmailInbox/EmailInbox_Incoming.ts
 create mode 100644 server/features/EmailInbox/EmailInbox_Outgoing.ts
 create mode 100644 server/features/EmailInbox/index.ts

diff --git a/app/api/server/index.js b/app/api/server/index.js
index 568664ede60..466b92e668f 100644
--- a/app/api/server/index.js
+++ b/app/api/server/index.js
@@ -38,5 +38,6 @@ import './v1/oauthapps';
 import './v1/custom-sounds';
 import './v1/custom-user-status';
 import './v1/instances';
+import './v1/email-inbox';
 
 export { API, APIClass, defaultRateLimiterOptions } from './api';
diff --git a/app/api/server/lib/emailInbox.js b/app/api/server/lib/emailInbox.js
new file mode 100644
index 00000000000..43d7a0e8f64
--- /dev/null
+++ b/app/api/server/lib/emailInbox.js
@@ -0,0 +1,79 @@
+import { EmailInbox } from '../../../models/server/raw';
+import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
+import { Users } from '../../../models';
+
+export async function findEmailInboxes({ userId, query = {}, pagination: { offset, count, sort } }) {
+	if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
+		throw new Error('error-not-allowed');
+	}
+	const cursor = EmailInbox.find(query, {
+		sort: sort || { name: 1 },
+		skip: offset,
+		limit: count,
+	});
+
+	const total = await cursor.count();
+
+	const emailInboxes = await cursor.toArray();
+
+	return {
+		emailInboxes,
+		count: emailInboxes.length,
+		offset,
+		total,
+	};
+}
+
+export async function findOneEmailInbox({ userId, _id }) {
+	if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
+		throw new Error('error-not-allowed');
+	}
+	return EmailInbox.findOneById(_id);
+}
+
+export async function insertOneOrUpdateEmailInbox(userId, emailInboxParams) {
+	const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams;
+
+	if (!_id) {
+		emailInboxParams._createdAt = new Date();
+		emailInboxParams._updatedAt = new Date();
+		emailInboxParams._createdBy = Users.findOne(userId, { fields: { username: 1 } });
+		return EmailInbox.insertOne(emailInboxParams);
+	}
+
+	const emailInbox = await findOneEmailInbox({ userId, id: _id });
+
+	if (!emailInbox) {
+		throw new Error('error-invalid-email-inbox');
+	}
+
+	const updateEmailInbox = {
+		$set: {
+			active,
+			name,
+			email,
+			description,
+			senderInfo,
+			smtp,
+			imap,
+			_updatedAt: new Date(),
+		},
+	};
+
+	if (department === 'All') {
+		updateEmailInbox.$unset = {
+			department: 1,
+		};
+	} else {
+		updateEmailInbox.$set.department = department;
+	}
+
+	return EmailInbox.updateOne({ _id }, updateEmailInbox);
+}
+
+export async function findOneEmailInboxByEmail({ userId, email }) {
+	if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
+		throw new Error('error-not-allowed');
+	}
+	return EmailInbox.findOne({ email });
+}
diff --git a/app/api/server/v1/email-inbox.js b/app/api/server/v1/email-inbox.js
new file mode 100644
index 00000000000..e7452fc5ffe
--- /dev/null
+++ b/app/api/server/v1/email-inbox.js
@@ -0,0 +1,131 @@
+import { check, Match } from 'meteor/check';
+
+import { API } from '../api';
+import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox';
+import { hasPermission } from '../../../authorization/server/functions/hasPermission';
+import { EmailInbox } from '../../../models';
+import Users from '../../../models/server/models/Users';
+import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing';
+
+API.v1.addRoute('email-inbox.list', { authRequired: true }, {
+	get() {
+		const { offset, count } = this.getPaginationItems();
+		const { sort, query } = this.parseJsonQuery();
+		const emailInboxes = Promise.await(findEmailInboxes({ userId: this.userId, query, pagination: { offset, count, sort } }));
+
+		return API.v1.success(emailInboxes);
+	},
+});
+
+API.v1.addRoute('email-inbox', { authRequired: true }, {
+	post() {
+		if (!hasPermission(this.userId, 'manage-email-inbox')) {
+			throw new Error('error-not-allowed');
+		}
+		check(this.bodyParams, {
+			_id: Match.Maybe(String),
+			name: String,
+			email: String,
+			active: Boolean,
+			description: Match.Maybe(String),
+			senderInfo: Match.Maybe(String),
+			department: Match.Maybe(String),
+			smtp: Match.ObjectIncluding({
+				password: String,
+				port: Number,
+				secure: Boolean,
+				server: String,
+				username: String,
+			}),
+			imap: Match.ObjectIncluding({
+				password: String,
+				port: Number,
+				secure: Boolean,
+				server: String,
+				username: String,
+			}),
+		});
+
+		const emailInboxParams = this.bodyParams;
+
+		const { _id } = emailInboxParams;
+
+		Promise.await(insertOneOrUpdateEmailInbox(this.userId, emailInboxParams));
+
+		return API.v1.success({ _id });
+	},
+});
+
+API.v1.addRoute('email-inbox/:_id', { authRequired: true }, {
+	get() {
+		check(this.urlParams, {
+			_id: String,
+		});
+
+		const { _id } = this.urlParams;
+		if (!_id) { throw new Error('error-invalid-param'); }
+		const emailInboxes = Promise.await(findOneEmailInbox({ userId: this.userId, _id }));
+
+		return API.v1.success(emailInboxes);
+	},
+	delete() {
+		if (!hasPermission(this.userId, 'manage-email-inbox')) {
+			throw new Error('error-not-allowed');
+		}
+		check(this.urlParams, {
+			_id: String,
+		});
+
+		const { _id } = this.urlParams;
+		if (!_id) { throw new Error('error-invalid-param'); }
+
+		const emailInboxes = EmailInbox.findOneById(_id);
+
+		if (!emailInboxes) {
+			return API.v1.notFound();
+		}
+		EmailInbox.removeById(_id);
+		return API.v1.success({ _id });
+	},
+});
+
+API.v1.addRoute('email-inbox.search', { authRequired: true }, {
+	get() {
+		if (!hasPermission(this.userId, 'manage-email-inbox')) {
+			throw new Error('error-not-allowed');
+		}
+		check(this.queryParams, {
+			email: String,
+		});
+
+		const { email } = this.queryParams;
+		const emailInbox = Promise.await(EmailInbox.findOne({ email }));
+
+		return API.v1.success({ emailInbox });
+	},
+});
+
+API.v1.addRoute('email-inbox.send-test/:_id', { authRequired: true }, {
+	post() {
+		if (!hasPermission(this.userId, 'manage-email-inbox')) {
+			throw new Error('error-not-allowed');
+		}
+		check(this.urlParams, {
+			_id: String,
+		});
+
+		const { _id } = this.urlParams;
+		if (!_id) { throw new Error('error-invalid-param'); }
+		const emailInbox = Promise.await(findOneEmailInbox({ userId: this.userId, _id }));
+
+		if (!emailInbox) {
+			return API.v1.notFound();
+		}
+
+		const user = Users.findOneById(this.userId);
+
+		Promise.await(sendTestEmailToInbox(emailInbox, user));
+
+		return API.v1.success({ _id });
+	},
+});
diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js
index e769cc2b500..7d5efbdca16 100644
--- a/app/authorization/server/startup.js
+++ b/app/authorization/server/startup.js
@@ -52,6 +52,7 @@ Meteor.startup(function() {
 		{ _id: 'leave-c',                            roles: ['admin', 'user', 'bot', 'anonymous', 'app'] },
 		{ _id: 'leave-p',                            roles: ['admin', 'user', 'bot', 'anonymous', 'app'] },
 		{ _id: 'manage-assets',                      roles: ['admin'] },
+		{ _id: 'manage-email-inbox',                 roles: ['admin'] },
 		{ _id: 'manage-emoji',                       roles: ['admin'] },
 		{ _id: 'manage-user-status',                 roles: ['admin'] },
 		{ _id: 'manage-outgoing-integrations',       roles: ['admin'] },
diff --git a/app/lib/server/lib/interceptDirectReplyEmails.js b/app/lib/server/lib/interceptDirectReplyEmails.js
index 0af20d2e2d1..0ef5361b66d 100644
--- a/app/lib/server/lib/interceptDirectReplyEmails.js
+++ b/app/lib/server/lib/interceptDirectReplyEmails.js
@@ -1,144 +1,29 @@
 import { Meteor } from 'meteor/meteor';
-import IMAP from 'imap';
 import POP3Lib from 'poplib';
 import { simpleParser } from 'mailparser';
 
 import { settings } from '../../../settings';
+import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor';
 
 import { processDirectEmail } from '.';
 
-export class IMAPIntercepter {
-	constructor() {
-		this.imap = new IMAP({
+export class IMAPIntercepter extends IMAPInterceptor {
+	constructor(imapConfig, options = {}) {
+		imapConfig = {
 			user: settings.get('Direct_Reply_Username'),
 			password: settings.get('Direct_Reply_Password'),
 			host: settings.get('Direct_Reply_Host'),
 			port: settings.get('Direct_Reply_Port'),
 			debug: settings.get('Direct_Reply_Debug') ? console.log : false,
 			tls: !settings.get('Direct_Reply_IgnoreTLS'),
-			connTimeout: 30000,
-			keepalive: true,
-		});
-
-		this.delete = settings.get('Direct_Reply_Delete');
-
-		// On successfully connected.
-		this.imap.on('ready', Meteor.bindEnvironment(() => {
-			if (this.imap.state !== 'disconnected') {
-				this.openInbox(Meteor.bindEnvironment((err) => {
-					if (err) {
-						throw err;
-					}
-					// fetch new emails & wait [IDLE]
-					this.getEmails();
-
-					// If new message arrived, fetch them
-					this.imap.on('mail', Meteor.bindEnvironment(() => {
-						this.getEmails();
-					}));
-				}));
-			} else {
-				console.log('IMAP didnot connected.');
-				this.imap.end();
-			}
-		}));
-
-		this.imap.on('error', (err) => {
-			console.log('Error occurred ...');
-			throw err;
-		});
-	}
-
-	openInbox(cb) {
-		this.imap.openBox('INBOX', false, cb);
-	}
-
-	start() {
-		this.imap.connect();
-	}
-
-	isActive() {
-		if (this.imap && this.imap.state && this.imap.state === 'disconnected') {
-			return false;
-		}
-
-		return true;
-	}
-
-	stop(callback = new Function()) {
-		this.imap.end();
-		this.imap.once('end', callback);
-	}
-
-	restart() {
-		this.stop(() => {
-			console.log('Restarting IMAP ....');
-			this.start();
-		});
-	}
-
-	// Fetch all UNSEEN messages and pass them for further processing
-	getEmails() {
-		this.imap.search(['UNSEEN'], Meteor.bindEnvironment((err, newEmails) => {
-			if (err) {
-				console.log(err);
-				throw err;
-			}
-
-			// newEmails => array containing serials of unseen messages
-			if (newEmails.length > 0) {
-				const f = this.imap.fetch(newEmails, {
-					// fetch headers & first body part.
-					bodies: ['HEADER.FIELDS (FROM TO DATE MESSAGE-ID)', '1'],
-					struct: true,
-					markSeen: true,
-				});
-
-				f.on('message', Meteor.bindEnvironment((msg, seqno) => {
-					const email = {};
-
-					msg.on('body', (stream, info) => {
-						let headerBuffer = '';
-						let bodyBuffer = '';
-
-						stream.on('data', (chunk) => {
-							if (info.which === '1') {
-								bodyBuffer += chunk.toString('utf8');
-							} else {
-								headerBuffer += chunk.toString('utf8');
-							}
-						});
+			...imapConfig,
+		};
 
-						stream.once('end', () => {
-							if (info.which === '1') {
-								email.body = bodyBuffer;
-							} else {
-								// parse headers
-								email.headers = IMAP.parseHeader(headerBuffer);
+		options.deleteAfterRead = settings.get('Direct_Reply_Delete');
 
-								email.headers.to = email.headers.to[0];
-								email.headers.date = email.headers.date[0];
-								email.headers.from = email.headers.from[0];
-							}
-						});
-					});
+		super(imapConfig, options);
 
-					// On fetched each message, pass it further
-					msg.once('end', Meteor.bindEnvironment(() => {
-						// delete message from inbox
-						if (this.delete) {
-							this.imap.seq.addFlags(seqno, 'Deleted', (err) => {
-								if (err) { console.log(`Mark deleted error: ${ err }`); }
-							});
-						}
-						processDirectEmail(email);
-					}));
-				}));
-				f.once('error', (err) => {
-					console.log(`Fetch error: ${ err }`);
-				});
-			}
-		}));
+		this.on('email', Meteor.bindEnvironment((email) => processDirectEmail(email)));
 	}
 }
 
diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html
index a4624bef2a7..c46ed0b1ec5 100644
--- a/app/livechat/client/views/app/tabbar/visitorInfo.html
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.html
@@ -39,6 +39,8 @@
 				<ul>
 					{{#with room}}
 						{{#if servedBy}}<li><strong>{{_ "Agent"}}</strong>: {{servedBy.username}}</li>{{/if}}
+						{{#if email}}<li><strong>{{_ "Email_Inbox"}}</strong>: {{email.inbox}}</li>{{/if}}
+						{{#if email}}<li><strong>{{_ "Email_subject"}}</strong>: {{email.subject}}</li>{{/if}}
 						{{#if facebook}}<li><i class="icon-facebook"></i>{{_ "Facebook_Page"}}: {{facebook.page.name}}</li>{{/if}}
 						{{#if sms}}<li><i class="i con-mobile"></i>{{_ "SMS_Enabled"}}</li>{{/if}}
 						{{#if topic}}<li><strong>{{_ "Topic"}}</strong>: {{{markdown topic}}}</li>{{/if}}
diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js
index 46e11e7f52f..f061207a35a 100644
--- a/app/livechat/client/views/app/tabbar/visitorInfo.js
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.js
@@ -202,7 +202,8 @@ Template.visitorInfo.helpers({
 	},
 
 	canSendTranscript() {
-		return hasPermission('send-omnichannel-chat-transcript');
+		const room = Template.instance().room.get();
+		return !room.email && hasPermission('send-omnichannel-chat-transcript');
 	},
 
 	roomClosedDateTime() {
diff --git a/app/livechat/server/business-hour/AbstractBusinessHour.ts b/app/livechat/server/business-hour/AbstractBusinessHour.ts
index 82f9bbc4905..32093d843ee 100644
--- a/app/livechat/server/business-hour/AbstractBusinessHour.ts
+++ b/app/livechat/server/business-hour/AbstractBusinessHour.ts
@@ -55,7 +55,7 @@ export abstract class AbstractBusinessHourType {
 		businessHourData.active = Boolean(businessHourData.active);
 		businessHourData = this.convertWorkHours(businessHourData);
 		if (businessHourData._id) {
-			await this.BusinessHourRepository.updateOne(businessHourData._id, businessHourData);
+			await this.BusinessHourRepository.updateOne({ _id: businessHourData._id }, { $set: businessHourData });
 			return businessHourData._id;
 		}
 		const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js
index 46ed51b7a14..33cf8dcf45e 100644
--- a/app/livechat/server/lib/QueueManager.js
+++ b/app/livechat/server/lib/QueueManager.js
@@ -6,6 +6,21 @@ import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from '.
 import { callbacks } from '../../../callbacks/server';
 import { RoutingManager } from './RoutingManager';
 
+
+const queueInquiry = async (room, inquiry, defaultAgent) => {
+	if (!defaultAgent) {
+		defaultAgent = RoutingManager.getMethod().delegateAgent(defaultAgent, inquiry);
+	}
+
+	inquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, defaultAgent);
+	if (inquiry.status === 'ready') {
+		return RoutingManager.delegateInquiry(inquiry, defaultAgent);
+	}
+
+	if (inquiry.status === 'queued') {
+		Meteor.defer(() => callbacks.run('livechat.chatQueued', room));
+	}
+};
 export const QueueManager = {
 	async requestRoom({ guest, message, roomInfo, agent, extraData }) {
 		check(message, Match.ObjectIncluding({
@@ -26,23 +41,38 @@ export const QueueManager = {
 		const name = (roomInfo && roomInfo.fname) || guest.name || guest.username;
 
 		const room = LivechatRooms.findOneById(createLivechatRoom(rid, name, guest, roomInfo, extraData));
-		let inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData }));
+		const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData }));
 
 		LivechatRooms.updateRoomCount();
 
-		if (!agent) {
-			agent = RoutingManager.getMethod().delegateAgent(agent, inquiry);
-		}
+		await queueInquiry(room, inquiry, agent);
 
-		inquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, agent);
-		if (inquiry.status === 'ready') {
-			return RoutingManager.delegateInquiry(inquiry, agent);
+		return room;
+	},
+
+	async unarchiveRoom(archivedRoom = {}) {
+		const { _id: rid, open, closedAt, fname: name, servedBy, v, departmentId: department, lastMessage: message } = archivedRoom;
+		if (!rid || !closedAt || !!open) {
+			return archivedRoom;
 		}
 
-		if (inquiry.status === 'queued') {
-			Meteor.defer(() => callbacks.run('livechat.chatQueued', room));
+		const oldInquiry = LivechatInquiry.findOneByRoomId(rid);
+		if (oldInquiry) {
+			LivechatInquiry.removeByRoomId(rid);
 		}
 
+		const guest = {
+			...v,
+			...department && { department },
+		};
+
+		const defaultAgent = servedBy && { agentId: servedBy._id, username: servedBy.username };
+
+		LivechatRooms.unarchiveOneById(rid);
+		const room = LivechatRooms.findOneById(rid);
+		const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message }));
+
+		await queueInquiry(room, inquiry, defaultAgent);
 		return room;
 	},
 };
diff --git a/app/mailer/server/api.js b/app/mailer/server/api.js
index 9b93aa4abd2..395af628b7d 100644
--- a/app/mailer/server/api.js
+++ b/app/mailer/server/api.js
@@ -109,7 +109,7 @@ export const sendNoWrap = ({ to, from, replyTo, subject, html, text, headers })
 	}
 
 	if (!text) {
-		text = stripHtml(html);
+		text = stripHtml(html).result;
 	}
 
 	if (settings.get('email_plain_text_only')) {
@@ -127,7 +127,7 @@ export const send = ({ to, from, replyTo, subject, html, text, data, headers })
 		subject: replace(subject, data),
 		text: text
 			? replace(text, data)
-			: stripHtml(replace(html, data)),
+			: stripHtml(replace(html, data)).result,
 		html: wrap(html, data),
 		headers,
 	});
diff --git a/app/models/server/index.js b/app/models/server/index.js
index fecf4de953b..efcd3790302 100644
--- a/app/models/server/index.js
+++ b/app/models/server/index.js
@@ -39,6 +39,7 @@ import ReadReceipts from './models/ReadReceipts';
 import LivechatExternalMessage from './models/LivechatExternalMessages';
 import OmnichannelQueue from './models/OmnichannelQueue';
 import Analytics from './models/Analytics';
+import EmailInbox from './models/EmailInbox';
 
 export { AppsLogsModel } from './models/apps-logs-model';
 export { AppsPersistenceModel } from './models/apps-persistence-model';
@@ -90,4 +91,5 @@ export {
 	LivechatInquiry,
 	Analytics,
 	OmnichannelQueue,
+	EmailInbox,
 };
diff --git a/app/models/server/models/EmailInbox.js b/app/models/server/models/EmailInbox.js
new file mode 100644
index 00000000000..490628be338
--- /dev/null
+++ b/app/models/server/models/EmailInbox.js
@@ -0,0 +1,27 @@
+import { Base } from './_Base';
+
+export class EmailInbox extends Base {
+	constructor() {
+		super('email_inbox');
+
+		this.tryEnsureIndex({ email: 1 }, { unique: true });
+	}
+
+	findOneById(_id, options) {
+		return this.findOne(_id, options);
+	}
+
+	create(data) {
+		return this.insert(data);
+	}
+
+	updateById(_id, data) {
+		return this.update({ _id }, data);
+	}
+
+	removeById(_id) {
+		return this.remove(_id);
+	}
+}
+
+export default new EmailInbox();
diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js
index a763bbee23d..a57b5e9f358 100644
--- a/app/models/server/models/LivechatRooms.js
+++ b/app/models/server/models/LivechatRooms.js
@@ -19,6 +19,7 @@ export class LivechatRooms extends Base {
 		this.tryEnsureIndex({ closedAt: 1 }, { sparse: true });
 		this.tryEnsureIndex({ servedBy: 1 }, { sparse: true });
 		this.tryEnsureIndex({ 'v.token': 1 }, { sparse: true });
+		this.tryEnsureIndex({ 'v.token': 1, 'email.thread': 1 }, { sparse: true });
 		this.tryEnsureIndex({ 'v._id': 1 }, { sparse: true });
 	}
 
@@ -168,6 +169,28 @@ export class LivechatRooms extends Base {
 		return this.findOne(query, options);
 	}
 
+	findOneByVisitorTokenAndEmailThread(visitorToken, emailThread, options) {
+		const query = {
+			t: 'l',
+			'v.token': visitorToken,
+			'email.thread': emailThread,
+		};
+
+		return this.findOne(query, options);
+	}
+
+	findOneOpenByVisitorTokenAndEmailThread(visitorToken, emailThread, options) {
+		const query = {
+			t: 'l',
+			open: true,
+			'v.token': visitorToken,
+			'email.thread': emailThread,
+		};
+
+		return this.findOne(query, options);
+	}
+
+
 	findOneLastServedAndClosedByVisitorToken(visitorToken, options = {}) {
 		const query = {
 			t: 'l',
@@ -706,6 +729,26 @@ export class LivechatRooms extends Base {
 
 		return this.update(query, update);
 	}
+
+	unarchiveOneById(roomId) {
+		const query = {
+			_id: roomId,
+			t: 'l',
+		};
+		const update = {
+			$set: {
+				open: true,
+			},
+			$unset: {
+				servedBy: 1,
+				closedAt: 1,
+				closedBy: 1,
+				closer: 1,
+			},
+		};
+
+		return this.update(query, update);
+	}
 }
 
 export default new LivechatRooms(Rooms.model, true);
diff --git a/app/models/server/raw/BaseRaw.ts b/app/models/server/raw/BaseRaw.ts
index f602de361df..f26ef02922b 100644
--- a/app/models/server/raw/BaseRaw.ts
+++ b/app/models/server/raw/BaseRaw.ts
@@ -1,4 +1,39 @@
-import { Collection, FindOneOptions, Cursor, WriteOpResult, DeleteWriteOpResultObject, FilterQuery, UpdateQuery, UpdateOneOptions } from 'mongodb';
+import {
+	Collection,
+	CollectionInsertOneOptions,
+	Cursor,
+	DeleteWriteOpResultObject,
+	FilterQuery,
+	FindOneOptions,
+	InsertOneWriteOpResult,
+	ObjectID,
+	ObjectId,
+	OptionalId,
+	UpdateManyOptions,
+	UpdateOneOptions,
+	UpdateQuery,
+	UpdateWriteOpResult,
+	WithId,
+	WriteOpResult,
+} from 'mongodb';
+
+// [extracted from @types/mongo] TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions
+type EnhancedOmit<T, K> = string | number extends keyof T
+	? T // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any"
+	: T extends any
+		? Pick<T, Exclude<keyof T, K>> // discriminated unions
+		: never;
+
+// [extracted from @types/mongo]
+type ExtractIdType<TSchema> = TSchema extends { _id: infer U } // user has defined a type for _id
+	? {} extends U
+		? Exclude<U, {}>
+		: unknown extends U
+			? ObjectId
+			: U
+	: ObjectId;
+
+type ModelOptionalId<T> = EnhancedOmit<T, '_id'> & { _id?: ExtractIdType<T> };
 
 interface ITrash {
 	__collection__: string;
@@ -70,6 +105,24 @@ export class BaseRaw<T> implements IBaseRaw<T> {
 		return this.col.update(filter, update, options);
 	}
 
+	updateOne(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateOneOptions & { multi?: boolean }): Promise<UpdateWriteOpResult> {
+		return this.col.updateOne(filter, update, options);
+	}
+
+	updateMany(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateManyOptions): Promise<UpdateWriteOpResult> {
+		return this.col.updateMany(filter, update, options);
+	}
+
+	insertOne(doc: ModelOptionalId<T>, options?: CollectionInsertOneOptions): Promise<InsertOneWriteOpResult<WithId<T>>> {
+		if (!doc._id || typeof doc._id !== 'string') {
+			const oid = new ObjectID();
+			doc = { _id: oid.toHexString(), ...doc };
+		}
+
+		// TODO reavaluate following type casting
+		return this.col.insertOne(doc as unknown as OptionalId<T>, options);
+	}
+
 	removeById(_id: string): Promise<DeleteWriteOpResultObject> {
 		const query: object = { _id };
 		return this.col.deleteOne(query);
diff --git a/app/models/server/raw/EmailInbox.ts b/app/models/server/raw/EmailInbox.ts
new file mode 100644
index 00000000000..1d8d008242f
--- /dev/null
+++ b/app/models/server/raw/EmailInbox.ts
@@ -0,0 +1,6 @@
+import { BaseRaw } from './BaseRaw';
+import { IEmailInbox } from '../../../../definition/IEmailInbox';
+
+export class EmailInboxRaw extends BaseRaw<IEmailInbox> {
+	//
+}
diff --git a/app/models/server/raw/LivechatBusinessHours.ts b/app/models/server/raw/LivechatBusinessHours.ts
index 09993b1697f..b48e53a8415 100644
--- a/app/models/server/raw/LivechatBusinessHours.ts
+++ b/app/models/server/raw/LivechatBusinessHours.ts
@@ -57,20 +57,6 @@ export class LivechatBusinessHoursRaw extends BaseRaw<ILivechatBusinessHour> {
 		});
 	}
 
-	async updateOne(_id: string, data: Omit<ILivechatBusinessHour, '_id'>): Promise<any> {
-		const query = {
-			_id,
-		};
-
-		const update = {
-			$set: {
-				...data,
-			},
-		};
-
-		return this.col.updateOne(query, update);
-	}
-
 	// TODO: Remove this function after remove the deprecated method livechat:saveOfficeHours
 	async updateDayOfGlobalBusinessHour(day: Omit<IBusinessHourWorkHour, 'code'>): Promise<any> {
 		return this.col.updateOne({
diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts
index 7c2a4d92ab5..243b627d5da 100644
--- a/app/models/server/raw/index.ts
+++ b/app/models/server/raw/index.ts
@@ -63,6 +63,8 @@ import { IntegrationHistoryRaw } from './IntegrationHistory';
 import IntegrationHistoryModel from '../models/IntegrationHistory';
 import OmnichannelQueueModel from '../models/OmnichannelQueue';
 import { OmnichannelQueueRaw } from './OmnichannelQueue';
+import EmailInboxModel from '../models/EmailInbox';
+import { EmailInboxRaw } from './EmailInbox';
 import { api } from '../../../../server/sdk/api';
 import { initWatchers } from '../../../../server/modules/watchers/watchers.module';
 
@@ -100,6 +102,7 @@ export const InstanceStatus = new InstanceStatusRaw(InstanceStatusModel.model.ra
 export const IntegrationHistory = new IntegrationHistoryRaw(IntegrationHistoryModel.model.rawCollection(), trashCollection);
 export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), trashCollection);
 export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection);
+export const EmailInbox = new EmailInboxRaw(EmailInboxModel.model.rawCollection(), trashCollection);
 
 const map = {
 	[Messages.col.collectionName]: MessagesModel,
@@ -116,6 +119,7 @@ const map = {
 	[InstanceStatus.col.collectionName]: InstanceStatusModel,
 	[IntegrationHistory.col.collectionName]: IntegrationHistoryModel,
 	[Integrations.col.collectionName]: IntegrationsModel,
+	[EmailInbox.col.collectionName]: EmailInboxModel,
 };
 
 if (!process.env.DISABLE_DB_WATCH) {
@@ -134,6 +138,7 @@ if (!process.env.DISABLE_DB_WATCH) {
 		InstanceStatus,
 		IntegrationHistory,
 		Integrations,
+		EmailInbox,
 	};
 
 	initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => {
diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js
index 86c3aff9eee..46856eeecea 100644
--- a/app/ui-message/client/messageBox/messageBox.js
+++ b/app/ui-message/client/messageBox/messageBox.js
@@ -106,16 +106,25 @@ Template.messageBox.onCreated(function() {
 });
 
 Template.messageBox.onRendered(function() {
-	const $input = $(this.find('.js-input-message'));
-	this.source = $input[0];
-	$input.on('dataChange', () => {
-		const messages = $input.data('reply') || [];
-		this.replyMessageData.set(messages);
-	});
+	let inputSetup = false;
+
 	this.autorun(() => {
 		const { rid, subscription } = Template.currentData();
 		const room = Session.get(`roomData${ rid }`);
 
+		if (!inputSetup) {
+			const $input = $(this.find('.js-input-message'));
+			this.source = $input[0];
+			if (this.source) {
+				inputSetup = true;
+			}
+			$input.on('dataChange', () => {
+				const messages = $input.data('reply') || [];
+				console.log('dataChange', messages);
+				this.replyMessageData.set(messages);
+			});
+		}
+
 		if (!room) {
 			return this.state.set({
 				room: false,
diff --git a/app/ui/client/views/app/lib/getCommonRoomEvents.js b/app/ui/client/views/app/lib/getCommonRoomEvents.js
index d4f5b3bfbac..3a28c14094a 100644
--- a/app/ui/client/views/app/lib/getCommonRoomEvents.js
+++ b/app/ui/client/views/app/lib/getCommonRoomEvents.js
@@ -9,12 +9,15 @@ import {
 	Layout,
 	MessageAction,
 } from '../../../../../ui-utils/client';
+import {
+	addMessageToList,
+} from '../../../../../ui-utils/client/lib/MessageAction';
 import { call } from '../../../../../ui-utils/client/lib/callMethod';
 import { promises } from '../../../../../promises/client';
 import { isURL } from '../../../../../utils/lib/isURL';
 import { openUserCard } from '../../../lib/UserCard';
 import { messageArgs } from '../../../../../ui-utils/client/lib/messageArgs';
-import { ChatMessage, Rooms } from '../../../../../models';
+import { ChatMessage, Rooms, Messages } from '../../../../../models';
 import { t } from '../../../../../utils/client';
 import { chatMessages } from '../room';
 import { EmojiEvents } from '../../../../../reactions/client/init';
@@ -220,6 +223,26 @@ export const getCommonRoomEvents = () => ({
 		input.value = msg;
 		input.focus();
 	},
+	async 'click .js-actionButton-respondWithQuotedMessage'(event, instance) {
+		const { rid } = instance.data;
+		const { id: msgId } = event.currentTarget;
+		const { $input } = chatMessages[rid];
+
+		if (!msgId) {
+			return;
+		}
+
+		const message = Messages.findOne({ _id: msgId });
+
+		let messages = $input.data('reply') || [];
+		messages = addMessageToList(messages, message);
+
+		$input
+			.focus()
+			.data('mention-user', false)
+			.data('reply', messages)
+			.trigger('dataChange');
+	},
 	async 'click .js-actionButton-sendMessage'(event, instance) {
 		const { rid } = instance.data;
 		const msg = event.currentTarget.value;
diff --git a/app/utils/lib/slashCommand.d.ts b/app/utils/lib/slashCommand.d.ts
new file mode 100644
index 00000000000..46ccc3ed68c
--- /dev/null
+++ b/app/utils/lib/slashCommand.d.ts
@@ -0,0 +1,3 @@
+export declare const slashCommand: {
+	add(command: string, callback: Function, options: object /* , result, providesPreview = false, previewer, previewCallback*/): void;
+};
diff --git a/client/components/AutoCompleteDepartment.js b/client/components/AutoCompleteDepartment.js
index 2e2c13e8b7f..acc8781d41a 100644
--- a/client/components/AutoCompleteDepartment.js
+++ b/client/components/AutoCompleteDepartment.js
@@ -9,7 +9,9 @@ export const AutoCompleteDepartment = React.memo((props) => {
 	const [filter, setFilter] = useState('');
 	const { value: data } = useEndpointData('livechat/department', useMemo(() => ({ text: filter }), [filter]));
 
-	const options = useMemo(() => (data && [{ value: 'all', label: t('All') }, ...data.departments.map((department) => ({ value: department._id, label: department.name }))]) || [{ value: 'all', label: t('All') }], [data, t]);
+	const { label } = props;
+
+	const options = useMemo(() => (data && [{ value: 'All', label: label && t('All') }, ...data.departments.map((department) => ({ value: department._id, label: department.name }))]) || [{ value: 'All', label: label || t('All') }], [data, label, t]);
 
 	return <AutoComplete
 		{...props}
diff --git a/client/components/Message/Attachments/ActionAttachtment.tsx b/client/components/Message/Attachments/ActionAttachtment.tsx
index e4da11ac081..24372941397 100644
--- a/client/components/Message/Attachments/ActionAttachtment.tsx
+++ b/client/components/Message/Attachments/ActionAttachtment.tsx
@@ -5,6 +5,7 @@ import { AttachmentProps } from '.';
 // DEPRECATED
 
 type Action = {
+	msgId?: string;
 	type: 'button';
 	text: string;
 	msg?: string;
@@ -12,7 +13,7 @@ type Action = {
 	image_url?: string;
 	is_webview?: true;
 	msg_in_chat_window?: true;
-	msg_processing_type?: 'sendMessage' | 'respondWithMessage';
+	msg_processing_type?: 'sendMessage' | 'respondWithMessage' | 'respondWithQuotedMessage';
 };
 
 export type ActionAttachmentProps = {
@@ -21,10 +22,10 @@ export type ActionAttachmentProps = {
 } & AttachmentProps;
 
 export const ActionAttachment: FC<ActionAttachmentProps> = ({ actions }) => <ButtonGroup mb='x4' {...{ small: true }}>{
-	actions.filter(({ type, msg_in_chat_window: msgInChatWindow, url, image_url: image, text }) => type === 'button' && (image || text) && (url || msgInChatWindow)).map(({ text, url, msg, msg_processing_type: processingType = 'sendMessage', image_url: image }, index) => {
+	actions.filter(({ type, msg_in_chat_window: msgInChatWindow, url, image_url: image, text }) => type === 'button' && (image || text) && (url || msgInChatWindow)).map(({ text, url, msgId, msg, msg_processing_type: processingType = 'sendMessage', image_url: image }, index) => {
 		const content = image ? <Box is='img' src={image} maxHeight={200}/> : text;
 		if (url) {
 			return <Button is='a' href={url} target='_blank' rel='noopener noreferrer' key={index} small>{content}</Button>;
 		}
-		return <Button className={`js-actionButton-${ processingType }`} key={index} small value={msg}>{content}</Button>;
+		return <Button className={`js-actionButton-${ processingType }`} key={index} small value={msg} id={msgId}>{content}</Button>;
 	})}</ButtonGroup>;
diff --git a/client/components/Sidebar.js b/client/components/Sidebar.js
index 05af4600013..aaafe865b9c 100644
--- a/client/components/Sidebar.js
+++ b/client/components/Sidebar.js
@@ -1,6 +1,6 @@
 import React, { useMemo } from 'react';
 import { css } from '@rocket.chat/css-in-js';
-import { Box, Icon, ActionButton } from '@rocket.chat/fuselage';
+import { Box, Icon, ActionButton, Tag } from '@rocket.chat/fuselage';
 
 import { useTranslation } from '../contexts/TranslationContext';
 import { useRoutePath } from '../contexts/RouterContext';
@@ -58,14 +58,14 @@ const GenericItem = ({ href, active, children, ...props }) => <Box
 	</Box>
 </Box>;
 
-const NavigationItem = ({ permissionGranted, pathGroup, pathSection, icon, label, currentPath }) => {
+const NavigationItem = ({ permissionGranted, pathGroup, pathSection, icon, label, currentPath, tag }) => {
 	const params = useMemo(() => ({ group: pathGroup }), [pathGroup]);
 	const path = useRoutePath(pathSection, params);
 	const isActive = path === currentPath || false;
 	if (permissionGranted && !permissionGranted()) { return null; }
 	return <Sidebar.GenericItem active={isActive} href={path} key={path}>
 		{icon && <Icon name={icon} size='x20' mi='x4'/>}
-		<Box withTruncatedText fontScale='p1' mi='x4' color='info'>{label}</Box>
+		<Box withTruncatedText fontScale='p1' mi='x4' color='info'>{label} {tag && <Tag style={{ display: 'inline', backgroundColor: '#000', color: '#FFF', marginLeft: 4 }}>{tag}</Tag>}</Box>
 	</Sidebar.GenericItem>;
 };
 
@@ -79,6 +79,7 @@ const ItemsAssembler = ({ items, currentPath }) => {
 		icon,
 		permissionGranted,
 		pathGroup,
+		tag,
 	}) => <Sidebar.NavigationItem
 		permissionGranted={permissionGranted}
 		pathGroup={pathGroup}
@@ -87,6 +88,7 @@ const ItemsAssembler = ({ items, currentPath }) => {
 		label={t(i18nLabel || name)}
 		key={i18nLabel || name}
 		currentPath={currentPath}
+		tag={t(tag)}
 	/>);
 };
 
diff --git a/client/views/admin/emailInbox/EmailInboxForm.js b/client/views/admin/emailInbox/EmailInboxForm.js
new file mode 100644
index 00000000000..1dbaa0a1333
--- /dev/null
+++ b/client/views/admin/emailInbox/EmailInboxForm.js
@@ -0,0 +1,361 @@
+import React, { useCallback, useState } from 'react';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import {
+	Accordion,
+	Button,
+	ButtonGroup,
+	TextInput,
+	TextAreaInput,
+	Field,
+	ToggleSwitch,
+	FieldGroup,
+	Box,
+	Margins,
+} from '@rocket.chat/fuselage';
+
+import { AutoCompleteDepartment } from '../../../components/AutoCompleteDepartment';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useRoute } from '../../../contexts/RouterContext';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
+import Page from '../../../components/Page';
+import { useForm } from '../../../hooks/useForm';
+import { useEndpointAction } from '../../../hooks/useEndpointAction';
+import { isEmail } from '../../../../app/utils';
+import { useEndpointData } from '../../../hooks/useEndpointData';
+import { AsyncStatePhase } from '../../../hooks/useAsyncState';
+import { FormSkeleton } from './Skeleton';
+import DeleteWarningModal from '../../../components/DeleteWarningModal';
+import { useSetModal } from '../../../contexts/ModalContext';
+import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
+
+
+const initialValues = {
+	active: true,
+	name: '',
+	email: '',
+	description: '',
+	senderInfo: '',
+	department: '',
+	// SMTP
+	smtpServer: '',
+	smtpPort: 587,
+	smtpUsername: '',
+	smtpPassword: '',
+	smtpSecure: false,
+	// IMAP
+	imapServer: '',
+	imapPort: 993,
+	imapUsername: '',
+	imapPassword: '',
+	imapSecure: false,
+};
+
+const getInitialValues = (data) => {
+	if (!data) {
+		return initialValues;
+	}
+
+	const {
+		active,
+		name,
+		email,
+		description,
+		senderInfo,
+		department,
+		smtp,
+		imap,
+	} = data;
+
+	return {
+		active: active ?? true,
+		name: name ?? '',
+		email: email ?? '',
+		description: description ?? '',
+		senderInfo: senderInfo ?? '',
+		department: department ?? '',
+		// SMTP
+		smtpServer: smtp.server ?? '',
+		smtpPort: smtp.port ?? 587,
+		smtpUsername: smtp.username ?? '',
+		smtpPassword: smtp.password ?? '',
+		smtpSecure: smtp.secure ?? false,
+		// IMAP
+		imapServer: imap.server ?? '',
+		imapPort: imap.port ?? 993,
+		imapUsername: imap.username ?? '',
+		imapPassword: imap.password ?? '',
+		imapSecure: imap.secure ?? false,
+	};
+};
+
+export function EmailInboxEditWithData({ id }) {
+	const t = useTranslation();
+	const { value: data, error, phase: state } = useEndpointData(`email-inbox/${ id }`);
+
+	if ([state].includes(AsyncStatePhase.LOADING)) {
+		return <FormSkeleton/>;
+	}
+
+	if (error || !data) {
+		return <Box mbs='x16'>{t('EmailInbox_not_found')}</Box>;
+	}
+
+	return <EmailInboxForm id={id} data={data} />;
+}
+
+export default function EmailInboxForm({ id, data }) {
+	const t = useTranslation();
+	const dispatchToastMessage = useToastMessageDispatch();
+	const setModal = useSetModal();
+	const [emailError, setEmailError] = useState();
+	const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data));
+
+	const {
+		handleActive,
+		handleName,
+		handleEmail,
+		handleDescription,
+		handleSenderInfo,
+		handleDepartment,
+		// SMTP
+		handleSmtpServer,
+		handleSmtpPort,
+		handleSmtpUsername,
+		handleSmtpPassword,
+		handleSmtpSecure,
+		// IMAP
+		handleImapServer,
+		handleImapPort,
+		handleImapUsername,
+		handleImapPassword,
+		handleImapSecure,
+	} = handlers;
+	const {
+		active,
+		name,
+		email,
+		description,
+		senderInfo,
+		department,
+		// SMTP
+		smtpServer,
+		smtpPort,
+		smtpUsername,
+		smtpPassword,
+		smtpSecure,
+		// IMAP
+		imapServer,
+		imapPort,
+		imapUsername,
+		imapPassword,
+		imapSecure,
+	} = values;
+
+	const router = useRoute('admin-email-inboxes');
+
+	const close = useCallback(() => router.push({}), [router]);
+
+	const saveEmailInbox = useEndpointAction('POST', 'email-inbox');
+	const deleteAction = useEndpointAction('DELETE', `email-inbox/${ id }`);
+	const emailAlreadyExistsAction = useEndpointAction('GET', `email-inbox.search?email=${ email }`);
+
+	useComponentDidUpdate(() => {
+		setEmailError(!isEmail(email) ? t('Validate_email_address') : null);
+	}, [t, email]);
+	useComponentDidUpdate(() => {
+		!email && setEmailError(null);
+	}, [email]);
+
+	const handleRemoveClick = useMutableCallback(async () => {
+		const result = await deleteAction();
+		if (result.success === true) {
+			close();
+		}
+	});
+
+	const handleDelete = useMutableCallback((e) => {
+		e.stopPropagation();
+		const onDeleteManager = async () => {
+			try {
+				await handleRemoveClick();
+				dispatchToastMessage({ type: 'success', message: t('Removed') });
+			} catch (error) {
+				dispatchToastMessage({ type: 'error', message: error });
+			}
+			setModal();
+		};
+
+		setModal(<DeleteWarningModal
+			onDelete={onDeleteManager}
+			onCancel={() => setModal()}
+		>{t('You_will_not_be_able_to_recover_email_inbox')}</DeleteWarningModal>);
+	});
+
+	const handleSave = useMutableCallback(async () => {
+		const smtp = { server: smtpServer, port: parseInt(smtpPort), username: smtpUsername, password: smtpPassword, secure: smtpSecure };
+		const imap = { server: imapServer, port: parseInt(imapPort), username: imapUsername, password: imapPassword, secure: imapSecure };
+		const payload = { active, name, email, description, senderInfo, department, smtp, imap };
+		if (id) {
+			payload._id = id;
+		}
+		try {
+			await saveEmailInbox(payload);
+			dispatchToastMessage({ type: 'success', message: t('Saved') });
+			close();
+		} catch (e) {
+			dispatchToastMessage({ type: 'error', message: e });
+		}
+	});
+
+
+	const checkEmailExists = useMutableCallback(async () => {
+		if (!email && !isEmail(email)) { return; }
+		const { emailInbox } = await emailAlreadyExistsAction();
+
+		if (!emailInbox || (id && emailInbox._id === id)) { return; }
+		setEmailError(t('Email_already_exists'));
+	});
+
+	const canSave = hasUnsavedChanges && name && (email && isEmail(email) && !emailError)
+	&& smtpServer && smtpPort && smtpUsername && smtpPassword
+	&& imapServer && imapPort && imapUsername && imapPassword;
+
+	return <Page.ScrollableContentWithShadow>
+		<Box maxWidth='x600' w='full' alignSelf='center'>
+			<Accordion>
+				<Accordion.Item defaultExpanded title={t('Inbox_Info')}>
+					<FieldGroup>
+						<Field>
+							<Field.Label display='flex' justifyContent='space-between' w='full'>
+								{t('Active')}
+								<ToggleSwitch checked={active} onChange={handleActive}/>
+							</Field.Label>
+						</Field>
+						<Field>
+							<Field.Label>{t('Name')}*</Field.Label>
+							<Field.Row>
+								<TextInput value={name} onChange={handleName} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Email')}*</Field.Label>
+							<Field.Row>
+								<TextInput onBlur={checkEmailExists} error={emailError} value={email} onChange={handleEmail} />
+							</Field.Row>
+							<Field.Error>
+								{t(emailError)}
+							</Field.Error>
+						</Field>
+						<Field>
+							<Field.Label>{t('Description')}</Field.Label>
+							<Field.Row>
+								<TextAreaInput value={description} rows={4} onChange={handleDescription} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Sender_Info')}</Field.Label>
+							<Field.Row>
+								<TextInput value={senderInfo} onChange={handleSenderInfo} placeholder={t('Optional')} />
+							</Field.Row>
+							<Field.Hint>
+								{t('Will_Appear_In_From')}
+							</Field.Hint>
+						</Field>
+						<Field>
+							<Field.Label>{t('Department')}</Field.Label>
+							<Field.Row>
+								<AutoCompleteDepartment value={department} onChange={handleDepartment} />
+							</Field.Row>
+							<Field.Hint>
+								{t('Only_Members_Selected_Department_Can_View_Channel')}
+							</Field.Hint>
+						</Field>
+					</FieldGroup>
+				</Accordion.Item>
+				<Accordion.Item title={t('Configure_Outgoing_Mail_SMTP')}>
+					<FieldGroup>
+						<Field>
+							<Field.Label>{t('Server')}*</Field.Label>
+							<Field.Row>
+								<TextInput value={smtpServer} onChange={handleSmtpServer} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Port')}*</Field.Label>
+							<Field.Row>
+								<TextInput type='number' value={smtpPort} onChange={handleSmtpPort} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Username')}*</Field.Label>
+							<Field.Row>
+								<TextInput value={smtpUsername} onChange={handleSmtpUsername} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Password')}*</Field.Label>
+							<Field.Row>
+								<TextInput type='password' value={smtpPassword} onChange={handleSmtpPassword} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label display='flex' justifyContent='space-between' w='full'>
+								{t('Connect_SSL_TLS')}
+								<ToggleSwitch checked={smtpSecure} onChange={handleSmtpSecure}/>
+							</Field.Label>
+						</Field>
+					</FieldGroup>
+				</Accordion.Item>
+				<Accordion.Item title={t('Configure_Incoming_Mail_IMAP')}>
+					<FieldGroup>
+						<Field>
+							<Field.Label>{t('Server')}*</Field.Label>
+							<Field.Row>
+								<TextInput value={imapServer} onChange={handleImapServer} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Port')}*</Field.Label>
+							<Field.Row>
+								<TextInput type='number' value={imapPort} onChange={handleImapPort} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Username')}*</Field.Label>
+							<Field.Row>
+								<TextInput value={imapUsername} onChange={handleImapUsername}/>
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label>{t('Password')}*</Field.Label>
+							<Field.Row>
+								<TextInput type='password' value={imapPassword} onChange={handleImapPassword} />
+							</Field.Row>
+						</Field>
+						<Field>
+							<Field.Label display='flex' justifyContent='space-between' w='full'>
+								{t('Connect_SSL_TLS')}
+								<ToggleSwitch checked={imapSecure} onChange={handleImapSecure} />
+							</Field.Label>
+						</Field>
+					</FieldGroup>
+				</Accordion.Item>
+				<Field>
+					<Field.Row>
+						<ButtonGroup stretch w='full'>
+							<Button onClick={close}>{t('Cancel')}</Button>
+							<Button disabled={!canSave} primary onClick={handleSave}>{t('Save')}</Button>
+						</ButtonGroup>
+					</Field.Row>
+					<Field.Row>
+						<Margins blockStart='x16'>
+							<ButtonGroup stretch w='full'>
+								{id && <Button primary danger onClick={handleDelete}>{t('Delete')}</Button>}
+							</ButtonGroup>
+						</Margins>
+					</Field.Row>
+				</Field>
+			</Accordion>
+		</Box>
+	</Page.ScrollableContentWithShadow>;
+}
diff --git a/client/views/admin/emailInbox/EmailInboxPage.js b/client/views/admin/emailInbox/EmailInboxPage.js
new file mode 100644
index 00000000000..805952f95d9
--- /dev/null
+++ b/client/views/admin/emailInbox/EmailInboxPage.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Button, Icon } from '@rocket.chat/fuselage';
+
+import Page from '../../../components/Page';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
+import EmailInboxTable from './EmailInboxTable';
+import EmailInboxForm, { EmailInboxEditWithData } from './EmailInboxForm';
+
+
+export function EmailInboxPage() {
+	const t = useTranslation();
+
+	const context = useRouteParameter('context');
+	const id = useRouteParameter('_id');
+
+	const emailInboxRoute = useRoute('admin-email-inboxes');
+
+	const handleNewButtonClick = () => {
+		emailInboxRoute.push({ context: 'new' });
+	};
+
+	return <Page flexDirection='row'>
+		<Page>
+			<Page.Header title={t('Email_Inboxes')}>
+				{context && <Button alignSelf='flex-end' onClick={() => emailInboxRoute.push({})}>
+					<Icon name='back'/>{t('Back')}
+				</Button>}
+				{!context && <Button primary onClick={handleNewButtonClick}>
+					<Icon name='plus'/> {t('New_Email_Inbox')}
+				</Button>}
+			</Page.Header>
+			<Page.Content>
+				{!context && <EmailInboxTable />}
+				{context === 'new' && <EmailInboxForm />}
+				{context === 'edit' && <EmailInboxEditWithData id={id} />}
+			</Page.Content>
+		</Page>
+	</Page>;
+}
+
+export default EmailInboxPage;
diff --git a/client/views/admin/emailInbox/EmailInboxRoute.js b/client/views/admin/emailInbox/EmailInboxRoute.js
new file mode 100644
index 00000000000..fc60932361a
--- /dev/null
+++ b/client/views/admin/emailInbox/EmailInboxRoute.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { usePermission } from '../../../contexts/AuthorizationContext';
+import NotAuthorizedPage from '../../../components/NotAuthorizedPage';
+import EmailInboxPage from './EmailInboxPage';
+
+function EmailInboxRoute() {
+	const canViewEmailInbox = usePermission('manage-email-inbox');
+
+	if (!canViewEmailInbox) {
+		return <NotAuthorizedPage />;
+	}
+
+	return <EmailInboxPage />;
+}
+
+export default EmailInboxRoute;
diff --git a/client/views/admin/emailInbox/EmailInboxTable.js b/client/views/admin/emailInbox/EmailInboxTable.js
new file mode 100644
index 00000000000..e4e5cc57958
--- /dev/null
+++ b/client/views/admin/emailInbox/EmailInboxTable.js
@@ -0,0 +1,73 @@
+import { Button, Table, Icon } from '@rocket.chat/fuselage';
+import React, { useMemo, useCallback, useState } from 'react';
+import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+
+import GenericTable from '../../../components/GenericTable';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useRoute } from '../../../contexts/RouterContext';
+import { useEndpointData } from '../../../hooks/useEndpointData';
+import { useEndpoint } from '../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
+
+export function SendTestButton({ id }) {
+	const t = useTranslation();
+
+	const dispatchToastMessage = useToastMessageDispatch();
+	const sendTest = useEndpoint('POST', `email-inbox.send-test/${ id }`);
+
+	return <Table.Cell fontScale='p1' color='hint' withTruncatedText>
+		<Button small ghost title={t('Send_Test_Email')} onClick={(e) => e.preventDefault() & e.stopPropagation() & sendTest() & dispatchToastMessage({ type: 'success', message: t('Email_sent') })}>
+			<Icon name='send' size='x20'/>
+		</Button>
+	</Table.Cell>;
+}
+
+const useQuery = ({ itemsPerPage, current }, [column, direction]) => useMemo(() => ({
+	sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }),
+	...itemsPerPage && { count: itemsPerPage },
+	...current && { offset: current },
+}), [column, current, direction, itemsPerPage]);
+
+function EmailInboxTable() {
+	const t = useTranslation();
+
+	const [params, setParams] = useState({ current: 0, itemsPerPage: 25 });
+	const [sort] = useState(['name', 'asc']);
+	const debouncedParams = useDebouncedValue(params, 500);
+	const debouncedSort = useDebouncedValue(sort, 500);
+	const query = useQuery(debouncedParams, debouncedSort);
+	const router = useRoute('admin-email-inboxes');
+
+	const onClick = useCallback((_id) => () => router.push({
+		context: 'edit',
+		_id,
+	}), [router]);
+
+
+	const header = useMemo(() => [
+		<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'}>{t('Name')}</GenericTable.HeaderCell>,
+		<GenericTable.HeaderCell key={'email'} direction={sort[1]} active={sort[0] === 'email'}>{t('Email')}</GenericTable.HeaderCell>,
+		<GenericTable.HeaderCell key={'active'} direction={sort[1]} active={sort[0] === 'active'}>{t('Active')}</GenericTable.HeaderCell>,
+		<GenericTable.HeaderCell key={'sendTest'} w='x60'></GenericTable.HeaderCell>,
+	].filter(Boolean), [sort, t]);
+
+	const { value: data } = useEndpointData('email-inbox.list', query);
+
+	const renderRow = useCallback(({ _id, name, email, active }) => <Table.Row action key={_id} onKeyDown={onClick(_id)} onClick={onClick(_id)} tabIndex={0} role='link'qa-room-id={_id}>
+		<Table.Cell withTruncatedText>{name}</Table.Cell>
+		<Table.Cell withTruncatedText>{email}</Table.Cell>
+		<Table.Cell withTruncatedText>{active ? t('Yes') : t('No')}</Table.Cell>
+		<SendTestButton id={_id} />
+	</Table.Row>, [onClick, t]);
+
+	return <GenericTable
+		header={header}
+		renderRow={renderRow}
+		results={data && data.emailInboxes}
+		total={data && data.total}
+		setParams={setParams}
+		params={params}
+	/>;
+}
+
+export default EmailInboxTable;
diff --git a/client/views/admin/emailInbox/Skeleton.js b/client/views/admin/emailInbox/Skeleton.js
new file mode 100644
index 00000000000..2fc5e9097c1
--- /dev/null
+++ b/client/views/admin/emailInbox/Skeleton.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import { Box, Skeleton } from '@rocket.chat/fuselage';
+
+export const FormSkeleton = (props) => <Box w='full' pb='x24' {...props}>
+	<Skeleton mbe='x8' />
+	<Skeleton mbe='x4'/>
+	<Skeleton mbe='x4'/>
+	<Skeleton mbe='x8'/>
+	<Skeleton mbe='x4'/>
+	<Skeleton mbe='x8'/>
+</Box>;
diff --git a/client/views/admin/routes.js b/client/views/admin/routes.js
index 1f00ff7370a..1d560236cb7 100644
--- a/client/views/admin/routes.js
+++ b/client/views/admin/routes.js
@@ -119,6 +119,11 @@ registerAdminRoute('/permissions/:context?/:_id?', {
 	lazyRouteComponent: () => import('./permissions/PermissionsRouter'),
 });
 
+registerAdminRoute('/email-inboxes/:context?/:_id?', {
+	name: 'admin-email-inboxes',
+	lazyRouteComponent: () => import('./emailInbox/EmailInboxRoute'),
+});
+
 Meteor.startup(() => {
 	registerAdminRoute('/:group+', {
 		name: 'admin',
diff --git a/client/views/admin/sidebarItems.js b/client/views/admin/sidebarItems.js
index 95d9b36ba04..a4e90461725 100644
--- a/client/views/admin/sidebarItems.js
+++ b/client/views/admin/sidebarItems.js
@@ -69,5 +69,11 @@ export const {
 		href: 'admin-marketplace',
 		i18nLabel: 'Marketplace',
 		permissionGranted: () => hasPermission(['manage-apps']),
+	}, {
+		icon: 'mail',
+		href: 'admin-email-inboxes',
+		i18nLabel: 'Email_Inboxes',
+		tag: 'Alpha',
+		permissionGranted: () => hasPermission(['manage-email-inbox']),
 	},
 ]);
diff --git a/client/views/omnichannel/currentChats/CurrentChatsPage.js b/client/views/omnichannel/currentChats/CurrentChatsPage.js
index 2744b029883..b915a09d959 100644
--- a/client/views/omnichannel/currentChats/CurrentChatsPage.js
+++ b/client/views/omnichannel/currentChats/CurrentChatsPage.js
@@ -145,7 +145,7 @@ const FilterByText = ({ setFilter, reload, ...props }) => {
 			</Box>
 			<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
 				<Label mb='x4'>{t('Department')}</Label>
-				<AutoCompleteDepartment value={department} onChange={handleDepartment}/>
+				<AutoCompleteDepartment value={department} onChange={handleDepartment} label={t('All')}/>
 			</Box>
 			<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
 				<Label mb='x4'>{t('Status')}</Label>
diff --git a/definition/IEmailInbox.ts b/definition/IEmailInbox.ts
new file mode 100644
index 00000000000..2f0ff390297
--- /dev/null
+++ b/definition/IEmailInbox.ts
@@ -0,0 +1,29 @@
+export interface IEmailInbox {
+	_id: string;
+	active: boolean;
+	name: string;
+	email: string;
+	description?: string;
+	senderInfo?: string;
+	department?: string;
+	smtp: {
+		server: string;
+		port: number;
+		username: string;
+		password: string;
+		secure: boolean;
+	};
+	imap: {
+		server: string;
+		port: number;
+		username: string;
+		password: string;
+		secure: boolean;
+	};
+	_createdAt: Date;
+	_createdBy: {
+		_id: string;
+		username: string;
+	};
+	_updatedAt: Date;
+}
diff --git a/ee/server/services/stream-hub/StreamHub.ts b/ee/server/services/stream-hub/StreamHub.ts
index 5ddf3c4b5cd..b559e43a7f7 100755
--- a/ee/server/services/stream-hub/StreamHub.ts
+++ b/ee/server/services/stream-hub/StreamHub.ts
@@ -15,6 +15,7 @@ import { IntegrationHistoryRaw } from '../../../../app/models/server/raw/Integra
 import { LivechatDepartmentAgentsRaw } from '../../../../app/models/server/raw/LivechatDepartmentAgents';
 import { IntegrationsRaw } from '../../../../app/models/server/raw/Integrations';
 import { PermissionsRaw } from '../../../../app/models/server/raw/Permissions';
+import { EmailInboxRaw } from '../../../../app/models/server/raw/EmailInbox';
 import { api } from '../../../../server/sdk/api';
 
 export class StreamHub extends ServiceClass implements IServiceClass {
@@ -41,6 +42,7 @@ export class StreamHub extends ServiceClass implements IServiceClass {
 		const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), Trash);
 		const IntegrationHistory = new IntegrationHistoryRaw(db.collection('rocketchat_integration_history'), Trash);
 		const Integrations = new IntegrationsRaw(db.collection('rocketchat_integrations'), Trash);
+		const EmailInbox = new EmailInboxRaw(db.collection('rocketchat_email_inbox'), Trash);
 
 		const models = {
 			Messages,
@@ -57,6 +59,7 @@ export class StreamHub extends ServiceClass implements IServiceClass {
 			InstanceStatus,
 			IntegrationHistory,
 			Integrations,
+			EmailInbox,
 		};
 
 		initWatchers(models, api.broadcast.bind(api), (model, fn) => {
diff --git a/package-lock.json b/package-lock.json
index 5c490e4c6b7..8032c380177 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10683,6 +10683,14 @@
 			"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==",
 			"dev": true
 		},
+		"@types/imap": {
+			"version": "0.8.33",
+			"resolved": "https://registry.npmjs.org/@types/imap/-/imap-0.8.33.tgz",
+			"integrity": "sha512-j9yzLtu3OV5YiOWpU33HT9K6RUOsmNSDDOpoflVpPZ586REK9Uyj+ZVUjYkOQJKMszQ7U5/fJWLRN4L56xE0xg==",
+			"requires": {
+				"@types/node": "*"
+			}
+		},
 		"@types/is-function": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.0.tgz",
@@ -10752,6 +10760,14 @@
 			"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
 			"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
 		},
+		"@types/mailparser": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.0.0.tgz",
+			"integrity": "sha512-LsGznUos/+iY83fVjoduIr3PUGfkgtcEvR7HqXpmiP4TsdZo6jf31EcmjDcROmluj1PDMhWRXOxy4ndkx78wUQ==",
+			"requires": {
+				"@types/node": "*"
+			}
+		},
 		"@types/markdown-to-jsx": {
 			"version": "6.11.3",
 			"resolved": "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz",
@@ -10893,6 +10909,14 @@
 				}
 			}
 		},
+		"@types/nodemailer": {
+			"version": "6.4.0",
+			"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz",
+			"integrity": "sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==",
+			"requires": {
+				"@types/node": "*"
+			}
+		},
 		"@types/normalize-package-data": {
 			"version": "2.4.0",
 			"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -11078,6 +11102,11 @@
 			"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
 			"dev": true
 		},
+		"@types/string-strip-html": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/@types/string-strip-html/-/string-strip-html-5.0.0.tgz",
+			"integrity": "sha512-+mdBIb+pxJ9SLwtjc2DgolMm8U7CG6qBdCevkjSsFB7ehJ0EExFd2ltKQ6m9CoKitqXwe6Tx5h+fAcklGQD0Bw=="
+		},
 		"@types/tapable": {
 			"version": "1.0.6",
 			"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
@@ -19808,21 +19837,21 @@
 			"dependencies": {
 				"abbrev": {
 					"version": "1.1.1",
-					"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+					"resolved": false,
 					"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
 					"dev": true,
 					"optional": true
 				},
 				"ansi-regex": {
 					"version": "2.1.1",
-					"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
 					"dev": true,
 					"optional": true
 				},
 				"aproba": {
 					"version": "1.2.0",
-					"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+					"resolved": false,
 					"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
 					"dev": true,
 					"optional": true
@@ -19840,14 +19869,14 @@
 				},
 				"balanced-match": {
 					"version": "1.0.0",
-					"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
 					"dev": true,
 					"optional": true
 				},
 				"brace-expansion": {
 					"version": "1.1.11",
-					"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+					"resolved": false,
 					"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
 					"dev": true,
 					"optional": true,
@@ -19865,28 +19894,28 @@
 				},
 				"code-point-at": {
 					"version": "1.1.0",
-					"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
 					"dev": true,
 					"optional": true
 				},
 				"concat-map": {
 					"version": "0.0.1",
-					"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
 					"dev": true,
 					"optional": true
 				},
 				"console-control-strings": {
 					"version": "1.1.0",
-					"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
 					"dev": true,
 					"optional": true
 				},
 				"core-util-is": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
 					"dev": true,
 					"optional": true
@@ -19910,14 +19939,14 @@
 				},
 				"delegates": {
 					"version": "1.0.0",
-					"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
 					"dev": true,
 					"optional": true
 				},
 				"detect-libc": {
 					"version": "1.0.3",
-					"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+					"resolved": false,
 					"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
 					"dev": true,
 					"optional": true
@@ -19934,14 +19963,14 @@
 				},
 				"fs.realpath": {
 					"version": "1.0.0",
-					"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
 					"dev": true,
 					"optional": true
 				},
 				"gauge": {
 					"version": "2.7.4",
-					"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+					"resolved": false,
 					"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
 					"dev": true,
 					"optional": true,
@@ -19973,7 +20002,7 @@
 				},
 				"has-unicode": {
 					"version": "2.0.1",
-					"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
 					"dev": true,
 					"optional": true
@@ -20000,7 +20029,7 @@
 				},
 				"inflight": {
 					"version": "1.0.6",
-					"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+					"resolved": false,
 					"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
 					"dev": true,
 					"optional": true,
@@ -20018,14 +20047,14 @@
 				},
 				"ini": {
 					"version": "1.3.5",
-					"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+					"resolved": false,
 					"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
 					"dev": true,
 					"optional": true
 				},
 				"is-fullwidth-code-point": {
 					"version": "1.0.0",
-					"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
 					"dev": true,
 					"optional": true,
@@ -20035,14 +20064,14 @@
 				},
 				"isarray": {
 					"version": "1.0.0",
-					"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
 					"dev": true,
 					"optional": true
 				},
 				"minimatch": {
 					"version": "3.0.4",
-					"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+					"resolved": false,
 					"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
 					"dev": true,
 					"optional": true,
@@ -20052,7 +20081,7 @@
 				},
 				"minimist": {
 					"version": "0.0.8",
-					"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+					"resolved": false,
 					"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
 					"dev": true,
 					"optional": true
@@ -20080,7 +20109,7 @@
 				},
 				"mkdirp": {
 					"version": "0.5.1",
-					"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
 					"dev": true,
 					"optional": true,
@@ -20135,7 +20164,7 @@
 				},
 				"nopt": {
 					"version": "4.0.1",
-					"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
 					"dev": true,
 					"optional": true,
@@ -20164,7 +20193,7 @@
 				},
 				"npmlog": {
 					"version": "4.1.2",
-					"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+					"resolved": false,
 					"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
 					"dev": true,
 					"optional": true,
@@ -20177,21 +20206,21 @@
 				},
 				"number-is-nan": {
 					"version": "1.0.1",
-					"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
 					"dev": true,
 					"optional": true
 				},
 				"object-assign": {
 					"version": "4.1.1",
-					"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
 					"dev": true,
 					"optional": true
 				},
 				"once": {
 					"version": "1.4.0",
-					"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
 					"dev": true,
 					"optional": true,
@@ -20201,21 +20230,21 @@
 				},
 				"os-homedir": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
 					"dev": true,
 					"optional": true
 				},
 				"os-tmpdir": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
 					"dev": true,
 					"optional": true
 				},
 				"osenv": {
 					"version": "0.1.5",
-					"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+					"resolved": false,
 					"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
 					"dev": true,
 					"optional": true,
@@ -20226,7 +20255,7 @@
 				},
 				"path-is-absolute": {
 					"version": "1.0.1",
-					"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
 					"dev": true,
 					"optional": true
@@ -20253,7 +20282,7 @@
 					"dependencies": {
 						"minimist": {
 							"version": "1.2.0",
-							"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+							"resolved": false,
 							"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
 							"dev": true,
 							"optional": true
@@ -20262,7 +20291,7 @@
 				},
 				"readable-stream": {
 					"version": "2.3.6",
-					"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+					"resolved": false,
 					"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
 					"dev": true,
 					"optional": true,
@@ -20295,14 +20324,14 @@
 				},
 				"safer-buffer": {
 					"version": "2.1.2",
-					"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+					"resolved": false,
 					"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
 					"dev": true,
 					"optional": true
 				},
 				"sax": {
 					"version": "1.2.4",
-					"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+					"resolved": false,
 					"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
 					"dev": true,
 					"optional": true
@@ -20316,21 +20345,21 @@
 				},
 				"set-blocking": {
 					"version": "2.0.0",
-					"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+					"resolved": false,
 					"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
 					"dev": true,
 					"optional": true
 				},
 				"signal-exit": {
 					"version": "3.0.2",
-					"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
 					"dev": true,
 					"optional": true
 				},
 				"string-width": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
 					"dev": true,
 					"optional": true,
@@ -20342,7 +20371,7 @@
 				},
 				"string_decoder": {
 					"version": "1.1.1",
-					"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+					"resolved": false,
 					"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
 					"dev": true,
 					"optional": true,
@@ -20352,7 +20381,7 @@
 				},
 				"strip-ansi": {
 					"version": "3.0.1",
-					"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
 					"dev": true,
 					"optional": true,
@@ -20362,7 +20391,7 @@
 				},
 				"strip-json-comments": {
 					"version": "2.0.1",
-					"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+					"resolved": false,
 					"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
 					"dev": true,
 					"optional": true
@@ -20385,7 +20414,7 @@
 				},
 				"util-deprecate": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
 					"dev": true,
 					"optional": true
@@ -20402,7 +20431,7 @@
 				},
 				"wrappy": {
 					"version": "1.0.2",
-					"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+					"resolved": false,
 					"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
 					"dev": true,
 					"optional": true
@@ -24794,7 +24823,6 @@
 			"version": "3.0.2",
 			"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
 			"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
-			"dev": true,
 			"requires": {
 				"uc.micro": "^1.0.1"
 			}
@@ -25391,13 +25419,10 @@
 						"safer-buffer": ">= 2.1.2 < 3.0.0"
 					}
 				},
-				"linkify-it": {
-					"version": "3.0.2",
-					"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
-					"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
-					"requires": {
-						"uc.micro": "^1.0.1"
-					}
+				"nodemailer": {
+					"version": "6.4.11",
+					"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.11.tgz",
+					"integrity": "sha512-BVZBDi+aJV4O38rxsUh164Dk1NCqgh6Cm0rQSb9SK/DHGll/DrCMnycVDD7msJgZCnmVa8ASo8EZzR7jsgTukQ=="
 				},
 				"tlds": {
 					"version": "1.208.0",
@@ -27981,9 +28006,9 @@
 			}
 		},
 		"nodemailer": {
-			"version": "6.4.11",
-			"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.11.tgz",
-			"integrity": "sha512-BVZBDi+aJV4O38rxsUh164Dk1NCqgh6Cm0rQSb9SK/DHGll/DrCMnycVDD7msJgZCnmVa8ASo8EZzR7jsgTukQ=="
+			"version": "6.4.17",
+			"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.17.tgz",
+			"integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ=="
 		},
 		"noop-logger": {
 			"version": "0.1.1",
@@ -30289,35 +30314,35 @@
 			"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
 		},
 		"ranges-apply": {
-			"version": "3.1.12",
-			"resolved": "https://registry.npmjs.org/ranges-apply/-/ranges-apply-3.1.12.tgz",
-			"integrity": "sha512-ojbyox6L2N165vXf6ml8+Q8bfqIezsQAURf9dIdTskre4yvcYerxA8IIK/c+AVpcc/pLP+4ZCD9kupUCgK/K1w==",
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/ranges-apply/-/ranges-apply-4.0.2.tgz",
+			"integrity": "sha512-i3h19Nz+lFI204WpkH2jOmr1LuC2zHTb/S8qoAOX4RU8CXa1ISVaXyFMMUsy+SF95hC6KtSd2feoLARgh9Yt0w==",
 			"requires": {
-				"ranges-merge": "^4.3.10"
+				"ranges-merge": "^6.2.0"
 			}
 		},
 		"ranges-merge": {
-			"version": "4.3.10",
-			"resolved": "https://registry.npmjs.org/ranges-merge/-/ranges-merge-4.3.10.tgz",
-			"integrity": "sha512-KK38l5CvC/CczjdT0smWu88cbspyNwnNRm6wOJTSXCU2e8tScOOoaZuw0PrnbS/K7IkzjuOjNmLa5xCsrWEA3Q==",
+			"version": "6.2.0",
+			"resolved": "https://registry.npmjs.org/ranges-merge/-/ranges-merge-6.2.0.tgz",
+			"integrity": "sha512-dI2NJkiZPu/xI19s4/0/TLWofnvt91FbAnICqcY3x8janbO7csAECMLdNG+0Q9hxQ9w7qT9NucT7y8eatOW2ew==",
 			"requires": {
-				"ranges-sort": "^3.12.2"
+				"ranges-sort": "^3.14.0"
 			}
 		},
 		"ranges-push": {
-			"version": "3.7.16",
-			"resolved": "https://registry.npmjs.org/ranges-push/-/ranges-push-3.7.16.tgz",
-			"integrity": "sha512-4Xf+m3tLFSYWc7vCPl7OOaR6so8V2f9LWQC/pmIYrMEqUdFSodqAULmDxO/WxLhMLfaVZ5ELQnNYjc34KZBC+g==",
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/ranges-push/-/ranges-push-4.0.2.tgz",
+			"integrity": "sha512-zmHoeMlrGCYMCSSIeGUtcwaQdvIuObUW3tJ22kniSRetaOGMHjfoqd8/ovH+u/gDFm0OS9GM1GenQImFXYy1OQ==",
 			"requires": {
-				"ranges-merge": "^4.3.10",
-				"string-collapse-leading-whitespace": "^2.0.22",
-				"string-trim-spaces-only": "^2.8.19"
+				"ranges-merge": "^6.2.0",
+				"string-collapse-leading-whitespace": "^4.0.0",
+				"string-trim-spaces-only": "^2.9.0"
 			}
 		},
 		"ranges-sort": {
-			"version": "3.12.2",
-			"resolved": "https://registry.npmjs.org/ranges-sort/-/ranges-sort-3.12.2.tgz",
-			"integrity": "sha512-220iIZ+1IFO+GnuoTqJ4PN7Re5eKpw3eY/zFEsJUw9grmtmHKdBkuSogJ3c6rpKT6sTg01E9Ay76deTGmmgQ4A=="
+			"version": "3.14.0",
+			"resolved": "https://registry.npmjs.org/ranges-sort/-/ranges-sort-3.14.0.tgz",
+			"integrity": "sha512-QoqzNY4yf/JtpBaOG12uxWxb/BUu9hPUucakOBrkgKA57GtmjJqMZYauqYryAVMztpkrrO7kqqzrIadYBXT53Q=="
 		},
 		"raw-body": {
 			"version": "2.3.3",
@@ -33439,31 +33464,31 @@
 			"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
 		},
 		"string-collapse-leading-whitespace": {
-			"version": "2.0.22",
-			"resolved": "https://registry.npmjs.org/string-collapse-leading-whitespace/-/string-collapse-leading-whitespace-2.0.22.tgz",
-			"integrity": "sha512-I3nI3FhfZK/xYbyhAH4+3Xl9K9OOkrH3NsamF6Loz0g0o0n0LE+Cl6E+aTSbpetVE/86AcOeYB/gKWWM5f8AVg=="
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/string-collapse-leading-whitespace/-/string-collapse-leading-whitespace-4.0.0.tgz",
+			"integrity": "sha512-AKnhq+wgx09Xrvp6fEYqucUcvXVcggwpA1hVv8e/zmg0Trhh8+KTuRBrsxEs7Nwnuy395xOEMoZHeigeH+eCVQ=="
 		},
 		"string-left-right": {
-			"version": "2.3.26",
-			"resolved": "https://registry.npmjs.org/string-left-right/-/string-left-right-2.3.26.tgz",
-			"integrity": "sha512-McFGIxAPf9AyPgvuipqk9NDvxxhWvk625GRrPFGAM+iWwHNT15GGdyjXY07h4eiw7Zkr0jKJXNF2fqXP0GBCHQ==",
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/string-left-right/-/string-left-right-3.0.1.tgz",
+			"integrity": "sha512-30bO/J4XHMFgk3I2h0ZUkhvzkryWbb/T4hxvBPyiw8DIfjgK0Relc/sla4LS/kK7UMFys5Sj69Cm+si4rx+nbQ==",
 			"requires": {
 				"lodash.clonedeep": "^4.5.0",
 				"lodash.isplainobject": "^4.0.6"
 			}
 		},
 		"string-strip-html": {
-			"version": "4.5.1",
-			"resolved": "https://registry.npmjs.org/string-strip-html/-/string-strip-html-4.5.1.tgz",
-			"integrity": "sha512-8zyUgZgehIoBWMUYuxZ75RoMWOKc1xlDi18sdENYnF3oI9XUUfK+9o1e7trEQ7SP8yEsMAvema7/oG/oEbb6lQ==",
+			"version": "7.0.3",
+			"resolved": "https://registry.npmjs.org/string-strip-html/-/string-strip-html-7.0.3.tgz",
+			"integrity": "sha512-88R5Dc4jr5z3EN7EQ14lMqwrMI4gpLrp8IneT+J0HBZsHCxTgSluL6hQm9PK+PEPGEIGURkMp407+Awk/BqJUg==",
 			"requires": {
 				"ent": "^2.2.0",
 				"lodash.isplainobject": "^4.0.6",
 				"lodash.trim": "^4.5.1",
 				"lodash.without": "^4.4.0",
-				"ranges-apply": "^3.1.11",
-				"ranges-push": "^3.7.15",
-				"string-left-right": "^2.3.25"
+				"ranges-apply": "^4.0.2",
+				"ranges-push": "^4.0.2",
+				"string-left-right": "^3.0.1"
 			}
 		},
 		"string-template": {
@@ -33472,9 +33497,9 @@
 			"integrity": "sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y="
 		},
 		"string-trim-spaces-only": {
-			"version": "2.8.19",
-			"resolved": "https://registry.npmjs.org/string-trim-spaces-only/-/string-trim-spaces-only-2.8.19.tgz",
-			"integrity": "sha512-jDg0UczZV6hkqPI60y0ODeZ5vPypUp1C/wPbJZ9sNQ0wxSA7wTBaSM2FtWak2SFVx4fMgcx3mjkO1y19i9paeQ=="
+			"version": "2.9.0",
+			"resolved": "https://registry.npmjs.org/string-trim-spaces-only/-/string-trim-spaces-only-2.9.0.tgz",
+			"integrity": "sha512-Ny/6ncfD52McLZbgYhhediuTzLGDjKd53H1QcJBFh1kbZRtsBntYmScJx5LeyPteGDRDbzP+qP6TyqJVj2ef9g=="
 		},
 		"string-width": {
 			"version": "1.0.2",
diff --git a/package.json b/package.json
index 9a7cf4e1f47..a803d9ddda7 100644
--- a/package.json
+++ b/package.json
@@ -148,7 +148,11 @@
 		"@rocket.chat/ui-kit": "^0.20.1",
 		"@slack/client": "^4.12.0",
 		"@types/fibers": "^3.1.0",
+		"@types/imap": "^0.8.33",
+		"@types/mailparser": "^3.0.0",
 		"@types/mkdirp": "^1.0.1",
+		"@types/nodemailer": "^6.4.0",
+		"@types/string-strip-html": "^5.0.0",
 		"@types/underscore.string": "0.0.38",
 		"@types/use-subscription": "^1.0.0",
 		"@types/xml-crypto": "^1.4.1",
@@ -229,6 +233,7 @@
 		"node-dogstatsd": "^0.0.7",
 		"node-gcm": "0.14.4",
 		"node-rsa": "^1.1.1",
+		"nodemailer": "^6.4.17",
 		"object-path": "^0.11.5",
 		"pdfjs-dist": "^2.4.456",
 		"photoswipe": "^4.1.3",
@@ -248,7 +253,7 @@
 		"simplebar-react": "^2.3.0",
 		"speakeasy": "^2.0.0",
 		"stream-buffers": "^3.0.2",
-		"string-strip-html": "^4.5.1",
+		"string-strip-html": "^7.0.3",
 		"styled-components": "^4.4.1",
 		"tar-stream": "^1.6.2",
 		"tinykeys": "^1.1.0",
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 0d3caed4806..59aa6d56837 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -243,6 +243,7 @@
   "Accounts_Verify_Email_For_External_Accounts": "Verify Email for External Accounts",
   "Action_required": "Action required",
   "Activate": "Activate",
+  "Active": "Active",
   "Active_users": "Active users",
   "Activity": "Activity",
   "Add": "Add",
@@ -841,11 +842,14 @@
   "Commit_details": "Commit Details",
   "Completed": "Completed",
   "Computer": "Computer",
+  "Configure_Incoming_Mail_IMAP": "Configure Incoming Mail (IMAP)",
+  "Configure_Outgoing_Mail_SMTP": "Configure Outgoing Mail (SMTP)",
   "Confirm_new_encryption_password": "Confirm new encryption password",
   "Confirm_new_password": "Confirm New Password",
   "Confirm_New_Password_Placeholder": "Please re-enter new password...",
   "Confirm_password": "Confirm your password",
   "Connect": "Connect",
+  "Connect_SSL_TLS": "Connect with SSL/TLS",
   "Connection_Closed": "Connection closed",
   "Connection_Reset": "Connection reset",
   "Connection_success": "LDAP Connection Successful",
@@ -1416,6 +1420,8 @@
   "Email_Footer_Description": "You may use the following placeholders: <br/><ul><li>[Site_Name] and [Site_URL] for the Application Name and URL respectively.</li></ul>",
   "Email_from": "From",
   "Email_Header_Description": "You may use the following placeholders: <br/><ul><li>[Site_Name] and [Site_URL] for the Application Name and URL respectively.</li></ul>",
+  "Email_Inbox": "Email Inbox",
+  "Email_Inboxes": "Email Inboxes",
   "Email_Notification_Mode": "Offline Email Notifications",
   "Email_Notification_Mode_All": "Every Mention/DM",
   "Email_Notification_Mode_Disabled": "Disabled",
@@ -1427,8 +1433,9 @@
   "email_plain_text_only": "Send only plain text emails",
   "email_style_description": "Avoid nested selectors",
   "email_style_label": "Email Style",
-  "Email_subject": "Subject",
+  "Email_subject": "Email Subject",
   "Email_verified": "Email verified",
+  "Email_sent": "Email sent",
   "Emails_sent_successfully!": "Emails sent successfully!",
   "Emoji": "Emoji",
   "Emoji_provided_by_JoyPixels": "Emoji provided by <strong>JoyPixels</strong>",
@@ -1533,6 +1540,7 @@
   "error-invalid-domain": "Invalid domain",
   "error-invalid-email": "Invalid email __email__",
   "error-invalid-email-address": "Invalid email address",
+  "error-invalid-email-inbox": "Invalid Email Inbox",
   "error-invalid-file-height": "Invalid file height",
   "error-invalid-file-type": "Invalid file type",
   "error-invalid-file-width": "Invalid file width",
@@ -1543,8 +1551,10 @@
   "error-invalid-method": "Invalid method",
   "error-invalid-name": "Invalid name",
   "error-invalid-password": "Invalid password",
+  "error-invalid-param": "Invalid param",
   "error-invalid-params": "Invalid params",
   "error-invalid-permission": "Invalid permission",
+  "error-invalid-port-number": "Invalid port number",
   "error-invalid-priority": "Invalid priority",
   "error-invalid-redirectUri": "Invalid redirectUri",
   "error-invalid-role": "Invalid role",
@@ -1989,6 +1999,7 @@
   "Importing_messages": "Importing messages",
   "Importing_users": "Importing users",
   "In_progress": "In progress",
+  "Inbox_Info": "Inbox Info",
   "Include_Offline_Agents": "Include offline agents",
   "Inclusive": "Inclusive",
   "Incoming_Livechats": "Queued Chats",
@@ -2468,6 +2479,8 @@
   "manage-assets": "Manage Assets",
   "manage-assets_description": "Permission to manage the server assets",
   "manage-cloud_description": "Manage Cloud",
+  "manage-email-inbox": "Manage Email Inbox",
+  "manage-email-inbox_description": "Permission to manage email inboxes",
   "manage-emoji": "Manage Emoji",
   "manage-emoji_description": "Permission to manage the server emojis",
   "manage-incoming-integrations": "Manage Incoming Integrations",
@@ -2733,6 +2746,7 @@
   "New_discussion": "New discussion",
   "New_discussion_first_message": "Usually, a discussion starts with a question, like \"How do I upload a picture?\"",
   "New_discussion_name": "A meaningful name for the discussion room",
+  "New_Email_Inbox": "New Email Inbox",
   "New_encryption_password": "New encryption password",
   "New_integration": "New integration",
   "New_line_message_compose_input": "`%s` - New line in message compose input",
@@ -2865,6 +2879,7 @@
   "Online": "Online",
   "Only_authorized_users_can_write_new_messages": "Only authorized users can write new messages",
   "Only_from_users": "Only prune content from these users (leave empty to prune everyone's content)",
+  "Only_Members_Selected_Department_Can_View_Channel": "Only members of selected department can view chats on this channel",
   "Only_On_Desktop": "Desktop mode (only sends with enter on desktop)",
   "Only_works_with_chrome_version_greater_50": "Only works with Chrome browser versions > 50",
   "Only_you_can_see_this_message": "Only you can see this message",
@@ -3139,6 +3154,7 @@
   "reply_counter_plural": "__counter__ replies",
   "Reply_in_direct_message": "Reply in Direct Message",
   "Reply_in_thread": "Reply in Thread",
+  "Reply_via_Email": "Reply via Email",
   "ReplyTo": "Reply-To",
   "Report": "Report",
   "Report_Abuse": "Report Abuse",
@@ -3401,18 +3417,22 @@
   "Send_request_on_offline_messages": "Send Request on Offline Messages",
   "Send_request_on_visitor_message": "Send Request on Visitor Messages",
   "Send_Test": "Send Test",
+  "Send_Test_Email": "Send test email",
   "Send_via_email": "Send via email",
+  "Send_via_Email_as_attachment": "Send via Email as attachment",
   "Send_Visitor_navigation_history_as_a_message": "Send Visitor Navigation History as a Message",
   "Send_visitor_navigation_history_on_request": "Send Visitor Navigation History on Request",
   "Send_welcome_email": "Send welcome email",
   "Send_your_JSON_payloads_to_this_URL": "Send your JSON payloads to this URL.",
   "send-many-messages": "Send Many Messages",
   "send-omnichannel-chat-transcript": "Send omnichannel conversation transcript",
+  "Sender_Info": "Sender Info",
   "Sending": "Sending...",
   "Sent_an_attachment": "Sent an attachment",
   "Sent_from": "Sent from",
   "Separate_multiple_words_with_commas": "Separate multiple words with commas",
   "Served_By": "Served By",
+  "Server": "Server",
   "Server_File_Path": "Server File Path",
   "Server_Folder_Path": "Server Folder Path",
   "Server_Info": "Server Info",
@@ -4137,6 +4157,7 @@
   "When_is_the_chat_busier?": "When is the chat busier?",
   "Where_are_the_messages_being_sent?": "Where are the messages being sent?",
   "Why_do_you_want_to_report_question_mark": "Why do you want to report?",
+  "Will_Appear_In_From": "Will appear in the From: header of emails you send.",
   "will_be_able_to": "will be able to",
   "Will_be_available_here_after_saving": "Will be available here after saving.",
   "Without_priority": "Without priority",
@@ -4185,6 +4206,7 @@
   "You_should_name_it_to_easily_manage_your_integrations": "You should name it to easily manage your integrations.",
   "You_will_be_asked_for_permissions": "You will be asked for permissions",
   "You_will_not_be_able_to_recover": "You will not be able to recover this message!",
+  "You_will_not_be_able_to_recover_email_inbox": "You will not be able to recover this email inbox",
   "You_will_not_be_able_to_recover_file": "You will not be able to recover this file!",
   "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "You won't receive email notifications because you have not verified your email.",
   "Your_e2e_key_has_been_reset": "Your e2e key has been reset.",
diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
index 0df150be6f1..4d9d089ddb4 100644
--- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
+++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
@@ -224,6 +224,7 @@
   "Accounts_UserAddedEmail_Description": "Você pode usar os seguintes espaços reservados: <br/><ul><li> [name], [fname], [lname] para o nome do usuário completo, primeiro nome ou sobrenome, respectivamente. </li><li> [email] para e-mail do usuário. </li><li> [Senha] para a senha do usuário. </li><li> [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. </li></ul>",
   "Accounts_UserAddedEmailSubject_Default": "Você foi adicionado ao [Site_Name]",
   "Activate": "Ativar",
+  "Active": "Ativo",
   "Activity": "Atividade",
   "Add": "Adicionar",
   "Add_agent": "Adicionar agente",
@@ -596,6 +597,7 @@
   "Channel_Archived": "Canal com o nome `#%s` foi arquivado com sucesso",
   "Channel_created": "Canal '#%s` criado.",
   "Channel_doesnt_exist": "O canal `#%s` não existe.",
+  "Channel_Info": "Informações do Canal",
   "Channel_name": "Nome do Canal",
   "Channel_Name_Placeholder": "Digite o nome do canal ...",
   "Channel_to_listen_on": "Canal para ouvir",
@@ -727,14 +729,17 @@
   "Common_Access": "Acesso comum",
   "Community": "Comunidade",
   "Compact": "Compacto",
-  "Condensed": "Condensado",
   "Completed": "Completo",
   "Computer": "Computador",
+  "Condensed": "Condensado",
+  "Configure_Incoming_Mail_IMAP": "Configurar protocolo de entrada (IMAP)",
+  "Configure_Outgoing_Mail_SMTP": "Configurar protocolo de saída (SMTP)",
   "Confirm_new_encryption_password": "Confirmar nova senha de criptografia",
   "Confirm_new_password": "Confirme a nova senha",
   "Confirm_New_Password_Placeholder": "Por favor, digite novamente a nova senha ...",
   "Confirm_password": "Confirmar a senha",
   "Connect": "Conectar",
+  "Connect_SSL_TLS": "Conectar com SSL/TLS",
   "Connection_Closed": "Conexão fechada",
   "Connection_Reset": "Conexão reset",
   "Consulting": "Consultar",
@@ -1026,6 +1031,7 @@
   "create-personal-access-tokens": "Criar tokens de acesso pessoal",
   "create-user": "Criar Usuário",
   "create-user_description": "Permissão para criar usuários",
+  "Created_by": "Criado por",
   "Created_at": "Data criação",
   "Created_at_s_by_s": "Criado em <strong>%s</strong> por <strong>%s</strong>",
   "Created_at_s_by_s_triggered_by_s": "Criado em <strong>%s</strong> por <strong>%s</strong> desencadeado por <strong>%s</strong>",
@@ -1246,6 +1252,7 @@
   "Email_Footer_Description": "Você pode usar os seguintes espaços reservados: <br/><ul><li> [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. </li></ul>",
   "Email_from": "De",
   "Email_Header_Description": "Você pode usar os seguintes espaços reservados: <br/><ul><li> [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. </li></ul>",
+  "Email_Inboxes": "Email Inboxes",
   "Email_Notification_Mode": "Notificações de E-mail Offline",
   "Email_Notification_Mode_All": "Cada Menção / Mensagem Direta",
   "Email_Notification_Mode_Disabled": "Desativado",
@@ -1253,6 +1260,7 @@
   "Email_Placeholder": "Por favor, indique o seu endereço de e-mail...",
   "Email_Placeholder_any": "Digite endereços de e-mail ...",
   "email_plain_text_only": "Enviar emails apenas em texto puro",
+  "Email_sent": "Email enviado",
   "email_style_description": "Evite seletores aninhados",
   "email_style_label": "Estilo do Email",
   "Email_subject": "Assunto",
@@ -1350,6 +1358,7 @@
   "error-invalid-domain": "Domínio inválido",
   "error-invalid-email": "__email__ não é um e-mail válido",
   "error-invalid-email-address": "Endereço de e-mail inválido",
+  "error-invalid-email-inbox": "Email Inbox inválido",
   "error-invalid-file-height": "Altura de arquivo inválida",
   "error-invalid-file-type": "Tipo de arquivo inválido",
   "error-invalid-file-width": "Altura de arquivo inválida",
@@ -2351,6 +2360,7 @@
   "New_discussion": "Nova discussão",
   "New_discussion_first_message": "Normalmente, uma discussão começa com uma pergunta, como \"Como faço o carregamento de uma imagem?\"",
   "New_discussion_name": "Um nome significativo para a sala de discussão",
+  "New_Email_Inbox": "Novo Email Inbox",
   "New_encryption_password": "Nova senha de criptografia",
   "New_integration": "Nova integração",
   "New_line_message_compose_input": "`%s` - Nova linha na mensagem compor a entrada",
@@ -2464,6 +2474,7 @@
   "online": "online",
   "Online": "Online",
   "Only_authorized_users_can_write_new_messages": "Somente usuários autorizados podem escrever novas mensagens",
+  "Only_Members_Selected_Department_Can_View_Channel": "Apenas membros do departamento selecionado poderão ver os chats neste canal.",
   "Only_from_users": "Apenas retire o conteúdo desses usuários (deixe em branco para remover o conteúdo de todos)",
   "Only_On_Desktop": "Modo Desktop (apenas envia com enter na área de trabalho)",
   "Only_you_can_see_this_message": "Apenas você pode ver esta mensagem",
@@ -2708,6 +2719,7 @@
   "reply_counter_plural": "__counter__ respostas",
   "Reply_in_direct_message": "Responder por Mensagem Direta",
   "Reply_in_thread": "Responder por Tópico",
+  "Reply_via_Email": "Responder por Email",
   "ReplyTo": "Responder para",
   "Report": "Reportar",
   "Report_Abuse": "Denunciar abuso",
@@ -2897,16 +2909,20 @@
   "Send_request_on_offline_messages": "Enviar requisição para mensagens off-line",
   "Send_request_on_visitor_message": "Enviar requisição para mensagens do Visitante",
   "Send_Test": "Enviar teste",
+  "Send_Test_Email": "Enviar email de teste",
+  "Send_via_Email_as_attachment": "Enviar por Email como anexo",
   "Send_Visitor_navigation_history_as_a_message": "Enviar histórico de navegação do visitante como uma mensagem",
   "Send_visitor_navigation_history_on_request": "Enviar histórico de navegação do visitante a pedido",
   "Send_welcome_email": "Enviar e-mail de boas-vindas",
   "Send_your_JSON_payloads_to_this_URL": "Envie seu payload  JSON para esta URL.",
   "send-many-messages": "Enviar muitas mensagens",
   "send-omnichannel-chat-transcript": "enviar transcrição de conversa omnichannel",
+  "Sender_Info": "Informações do Remetente",
   "Sending": "Enviando ...",
   "Sent_an_attachment": "Enviou um anexo",
   "Sent_from": "Enviado de",
   "Served_By": "Atendido Por",
+  "Server": "Servidor",
   "Server_Info": "Informações do servidor",
   "Server_Type": "Tipo de servidor",
   "Service": "Serviço",
@@ -3526,6 +3542,7 @@
   "Welcome_to": "Bem-vindo ao __Site_Name__",
   "Welcome_to_the": "Bem-vindo ao",
   "Why_do_you_want_to_report_question_mark": "Por que você quer denunciar?",
+  "Will_Appear_In_From": "Aparecerá no cabeçalho dos e-mails que você enviar.",
   "will_be_able_to": "poderá",
   "Without_priority": "Sem prioridade",
   "Worldwide": "Em todo o mundo",
@@ -3570,6 +3587,7 @@
   "You_should_inform_one_url_at_least": "Você deve definir pelo menos uma URL.",
   "You_should_name_it_to_easily_manage_your_integrations": "Você deve nomeá-lo para gerenciar facilmente as suas integrações.",
   "You_will_not_be_able_to_recover": "Você não será capaz de recuperar essa mensagem!",
+  "You_will_not_be_able_to_recover_email_inbox": "Você não será capaz de recuperar esse email inbox",
   "You_will_not_be_able_to_recover_file": "Não será possível recuperar esse arquivo!",
   "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "Você não receberá notificações de e-mail, porque você não confirmou seu e-mail.",
   "Your_email_has_been_queued_for_sending": "Seu e-mail foi colocado na fila para envio",
diff --git a/server/email/IMAPInterceptor.ts b/server/email/IMAPInterceptor.ts
new file mode 100644
index 00000000000..a8d631ae916
--- /dev/null
+++ b/server/email/IMAPInterceptor.ts
@@ -0,0 +1,146 @@
+import { EventEmitter } from 'events';
+
+import IMAP from 'imap';
+import type Connection from 'imap';
+import { simpleParser, ParsedMail } from 'mailparser';
+
+type IMAPOptions = {
+	deleteAfterRead: boolean;
+	filter: any[];
+	rejectBeforeTS?: Date;
+	markSeen: boolean;
+}
+
+export declare interface IMAPInterceptor {
+	on(event: 'email', listener: (email: ParsedMail) => void): this;
+	on(event: string, listener: Function): this;
+}
+
+export class IMAPInterceptor extends EventEmitter {
+	private imap: IMAP;
+
+	constructor(
+		imapConfig: IMAP.Config,
+		private options: IMAPOptions = {
+			deleteAfterRead: false,
+			filter: ['UNSEEN'],
+			markSeen: true,
+		},
+	) {
+		super();
+
+		this.imap = new IMAP({
+			connTimeout: 30000,
+			keepalive: true,
+			...imapConfig,
+		});
+
+		// On successfully connected.
+		this.imap.on('ready', () => {
+			if (this.imap.state !== 'disconnected') {
+				this.openInbox((err) => {
+					if (err) {
+						throw err;
+					}
+					// fetch new emails & wait [IDLE]
+					this.getEmails();
+
+					// If new message arrived, fetch them
+					this.imap.on('mail', () => {
+						this.getEmails();
+					});
+				});
+			} else {
+				this.log('IMAP did not connected.');
+				this.imap.end();
+			}
+		});
+
+		this.imap.on('error', (err: Error) => {
+			this.log('Error occurred ...');
+			throw err;
+		});
+	}
+
+	log(...msg: any[]): void {
+		console.log(...msg);
+	}
+
+	openInbox(cb: (error: Error, mailbox: Connection.Box) => void): void {
+		this.imap.openBox('INBOX', false, cb);
+	}
+
+	start(): void {
+		this.imap.connect();
+	}
+
+	isActive(): boolean {
+		if (this.imap && this.imap.state && this.imap.state === 'disconnected') {
+			return false;
+		}
+
+		return true;
+	}
+
+	stop(callback = new Function()): void {
+		this.imap.end();
+		this.imap.once('end', callback);
+	}
+
+	restart(): void {
+		this.stop(() => {
+			this.log('Restarting IMAP ....');
+			this.start();
+		});
+	}
+
+	// Fetch all UNSEEN messages and pass them for further processing
+	getEmails(): void {
+		this.imap.search(this.options.filter, (err, newEmails) => {
+			if (err) {
+				this.log(err);
+				throw err;
+			}
+
+			// newEmails => array containing serials of unseen messages
+			if (newEmails.length > 0) {
+				const fetch = this.imap.fetch(newEmails, {
+					bodies: ['HEADER', 'TEXT', ''],
+					struct: true,
+					markSeen: this.options.markSeen,
+				});
+
+				fetch.on('message', (msg, seqno) => {
+					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);
+								return;
+							}
+
+							this.emit('email', email);
+						});
+					});
+
+					// On fetched each message, pass it further
+					msg.once('end', () => {
+						// delete message from inbox
+						if (this.options.deleteAfterRead) {
+							this.imap.seq.addFlags(seqno, 'Deleted', (err) => {
+								if (err) { this.log(`Mark deleted error: ${ err }`); }
+							});
+						}
+					});
+				});
+
+				fetch.once('error', (err) => {
+					this.log(`Fetch error: ${ err }`);
+				});
+			}
+		});
+	}
+}
diff --git a/server/features/EmailInbox/EmailInbox.ts b/server/features/EmailInbox/EmailInbox.ts
new file mode 100644
index 00000000000..21ffbfdffbe
--- /dev/null
+++ b/server/features/EmailInbox/EmailInbox.ts
@@ -0,0 +1,69 @@
+import { Meteor } from 'meteor/meteor';
+import nodemailer from 'nodemailer';
+import Mail from 'nodemailer/lib/mailer';
+
+import { EmailInbox } from '../../../app/models/server/raw';
+import { IMAPInterceptor } from '../../email/IMAPInterceptor';
+import { IEmailInbox } from '../../../definition/IEmailInbox';
+import { onEmailReceived } from './EmailInbox_Incoming';
+
+export type Inbox = {
+	imap: IMAPInterceptor;
+	smtp: Mail;
+	config: IEmailInbox;
+}
+
+export const inboxes = new Map<string, Inbox>();
+
+export async function configureEmailInboxes(): Promise<void> {
+	const emailInboxesCursor = EmailInbox.find({
+		active: true,
+	});
+
+	for (const { imap } of inboxes.values()) {
+		imap.stop();
+	}
+
+	inboxes.clear();
+
+	for await (const emailInboxRecord of emailInboxesCursor) {
+		console.log('Setting up email interceptor for', emailInboxRecord.email);
+
+		const imap = new IMAPInterceptor({
+			password: emailInboxRecord.imap.password,
+			user: emailInboxRecord.imap.username,
+			host: emailInboxRecord.imap.server,
+			port: emailInboxRecord.imap.port,
+			tls: emailInboxRecord.imap.secure,
+			tlsOptions: {
+				rejectUnauthorized: false,
+			},
+			// debug: (...args: any[]): void => console.log(...args),
+		}, {
+			deleteAfterRead: false,
+			filter: [['UNSEEN'], ['SINCE', emailInboxRecord._updatedAt]],
+			rejectBeforeTS: emailInboxRecord._updatedAt,
+			markSeen: true,
+		});
+
+		imap.on('email', Meteor.bindEnvironment((email) => onEmailReceived(email, emailInboxRecord.email, emailInboxRecord.department)));
+
+		imap.start();
+
+		const smtp = nodemailer.createTransport({
+			host: emailInboxRecord.smtp.server,
+			port: emailInboxRecord.smtp.port,
+			secure: emailInboxRecord.smtp.secure,
+			auth: {
+				user: emailInboxRecord.smtp.username,
+				pass: emailInboxRecord.smtp.password,
+			},
+		});
+
+		inboxes.set(emailInboxRecord.email, { imap, smtp, config: emailInboxRecord });
+	}
+}
+
+Meteor.startup(() => {
+	configureEmailInboxes();
+});
diff --git a/server/features/EmailInbox/EmailInbox_Incoming.ts b/server/features/EmailInbox/EmailInbox_Incoming.ts
new file mode 100644
index 00000000000..d7927f06db3
--- /dev/null
+++ b/server/features/EmailInbox/EmailInbox_Incoming.ts
@@ -0,0 +1,203 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import stripHtml from 'string-strip-html';
+import { Random } from 'meteor/random';
+import { ParsedMail, Attachment } from 'mailparser';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+
+import { Livechat } from '../../../app/livechat/server/lib/Livechat';
+import { LivechatRooms, LivechatVisitors, Messages } from '../../../app/models/server';
+import { FileUpload } from '../../../app/file-upload/server';
+import { QueueManager } from '../../../app/livechat/server/lib/QueueManager';
+import { settings } from '../../../app/settings/server';
+
+type FileAttachment = {
+	title: string;
+	title_link: string;
+	image_url?: string;
+	image_type?: string;
+	image_size?: string;
+	image_dimensions?: string;
+	audio_url?: string;
+	audio_type?: string;
+	audio_size?: string;
+	video_url?: string;
+	video_type?: string;
+	video_size?: string;
+}
+
+const language = settings.get('Language') || 'en';
+const t = (s: string): string => TAPi18n.__(s, { lng: language });
+
+function getGuestByEmail(email: string, name: string, department?: string): any {
+	const guest = LivechatVisitors.findOneGuestByEmailAddress(email);
+
+	if (guest) {
+		return guest;
+	}
+
+	const userId = Livechat.registerGuest({
+		token: Random.id(),
+		name: name || email,
+		email,
+		department,
+		phone: undefined,
+		username: undefined,
+		connectionData: undefined,
+	});
+
+	const newGuest = LivechatVisitors.findOneById(userId, {});
+	if (newGuest) {
+		return newGuest;
+	}
+
+	throw new Error('Error getting guest');
+}
+
+async function uploadAttachment(attachment: Attachment, rid: string, visitorToken: string): Promise<FileAttachment> {
+	const details = {
+		name: attachment.filename,
+		size: attachment.size,
+		type: attachment.contentType,
+		rid,
+		visitorToken,
+	};
+
+	const fileStore = FileUpload.getStore('Uploads');
+	return new Promise((resolve, reject) => {
+		fileStore.insert(details, attachment.content, function(err: any, file: any) {
+			if (err) {
+				reject(new Error(err));
+			}
+
+			const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`);
+
+			const attachment: FileAttachment = {
+				title: file.name,
+				title_link: url,
+			};
+
+			if (/^image\/.+/.test(file.type)) {
+				attachment.image_url = url;
+				attachment.image_type = file.type;
+				attachment.image_size = file.size;
+				attachment.image_dimensions = file.identify != null ? file.identify.size : undefined;
+			}
+
+			if (/^audio\/.+/.test(file.type)) {
+				attachment.audio_url = url;
+				attachment.audio_type = file.type;
+				attachment.audio_size = file.size;
+			}
+
+			if (/^video\/.+/.test(file.type)) {
+				attachment.video_url = url;
+				attachment.video_type = file.type;
+				attachment.video_size = file.size;
+			}
+
+			resolve(attachment);
+		});
+	});
+}
+
+export async function onEmailReceived(email: ParsedMail, inbox: string, department?: string): Promise<void> {
+	if (!email.from?.value?.[0]?.address) {
+		return;
+	}
+
+	const references = typeof email.references === 'string' ? [email.references] : email.references;
+
+	const thread = references?.[0] ?? email.messageId;
+
+	const guest = getGuestByEmail(email.from.value[0].address, email.from.value[0].name, department);
+
+	let room = LivechatRooms.findOneByVisitorTokenAndEmailThread(guest.token, thread, {});
+	if (room?.closedAt) {
+		room = await QueueManager.unarchiveRoom(room);
+	}
+
+	let msg = email.text;
+
+	if (email.html) {
+		// Try to remove the signature and history
+		msg = stripHtml(email.html.replace(/<div name="messageSignatureSection.+/s, '')).result;
+	}
+
+	const rid = room?._id ?? Random.id();
+	const msgId = Random.id();
+
+	Livechat.sendMessage({
+		guest,
+		message: {
+			_id: msgId,
+			groupable: false,
+			msg,
+			attachments: [
+				{
+					actions: [{
+						type: 'button',
+						text: t('Reply_via_Email'),
+						msg: 'msg',
+						msgId,
+						msg_in_chat_window: true,
+						msg_processing_type: 'respondWithQuotedMessage',
+					}],
+				},
+			],
+			blocks: [{
+				type: 'context',
+				elements: [{
+					type: 'mrkdwn',
+					text: `**${ t('From') }:** ${ email.from.text }\n**${ t('Subject') }:** ${ email.subject }`,
+				}],
+			}, {
+				type: 'section',
+				text: {
+					type: 'mrkdwn',
+					text: msg,
+				},
+			}],
+			rid,
+			email: {
+				references,
+				messageId: email.messageId,
+			},
+		},
+		roomInfo: {
+			email: {
+				inbox,
+				thread,
+				replyTo: email.from.value[0].address,
+				subject: email.subject,
+			},
+		},
+		agent: undefined,
+	}).then(async () => {
+		if (!email.attachments.length) {
+			return;
+		}
+
+		const attachments = [];
+		for await (const attachment of email.attachments) {
+			if (attachment.type !== 'attachment') {
+				continue;
+			}
+
+			try {
+				attachments.push(await uploadAttachment(attachment, rid, guest.token));
+			} catch (e) {
+				console.error('Error uploading attachment from email', e);
+			}
+		}
+
+		Messages.update({ _id: msgId }, {
+			$addToSet: {
+				attachments: {
+					$each: attachments,
+				},
+			},
+		});
+	}).catch((error) => {
+		console.log('Error receiving Email: %s', error.message);
+	});
+}
diff --git a/server/features/EmailInbox/EmailInbox_Outgoing.ts b/server/features/EmailInbox/EmailInbox_Outgoing.ts
new file mode 100644
index 00000000000..03c297a59b1
--- /dev/null
+++ b/server/features/EmailInbox/EmailInbox_Outgoing.ts
@@ -0,0 +1,238 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import Mail from 'nodemailer/lib/mailer';
+import { Match } from 'meteor/check';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+
+import { callbacks } from '../../../app/callbacks/server';
+import { IEmailInbox } from '../../../definition/IEmailInbox';
+import { IUser } from '../../../definition/IUser';
+import { FileUpload } from '../../../app/file-upload/server';
+import { slashCommands } from '../../../app/utils/server';
+import { Messages, Rooms, Uploads, Users } from '../../../app/models/server';
+import { Inbox, inboxes } from './EmailInbox';
+import { sendMessage } from '../../../app/lib/server/functions/sendMessage';
+import { settings } from '../../../app/settings/server';
+
+const livechatQuoteRegExp = /^\[\s\]\(https?:\/\/.+\/live\/.+\?msg=(?<id>.+?)\)\s(?<text>.+)/s;
+
+const user: IUser = Users.findOneById('rocket.cat');
+
+const language = settings.get('Language') || 'en';
+const t = (s: string): string => TAPi18n.__(s, { lng: language });
+
+const sendErrorReplyMessage = (error: string, options: any): void => {
+	if (!options?.rid || !options?.msgId) {
+		return;
+	}
+
+	const message = {
+		groupable: false,
+		msg: `@${ options.sender } something went wrong when replying email, sorry. **Error:**: ${ error }`,
+		_id: String(Date.now()),
+		rid: options.rid,
+		ts: new Date(),
+	};
+
+	sendMessage(user, message, { _id: options.rid });
+};
+
+function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): void {
+	inbox.smtp.sendMail({
+		from: inbox.config.senderInfo ? {
+			name: inbox.config.senderInfo,
+			address: inbox.config.email,
+		} : inbox.config.email,
+		...mail,
+	}).then((info) => {
+		console.log('Message sent: %s', info.messageId);
+	}).catch((error) => {
+		console.log('Error sending Email reply: %s', error.message);
+
+		if (!options?.msgId) {
+			return;
+		}
+
+		sendErrorReplyMessage(error.message, options);
+	});
+}
+
+slashCommands.add('sendEmailAttachment', (command: any, params: string) => {
+	if (command !== 'sendEmailAttachment' || !Match.test(params, String)) {
+		return;
+	}
+
+	const message = Messages.findOneById(params.trim());
+
+	if (!message || !message.file) {
+		return;
+	}
+
+	const room = Rooms.findOneById(message.rid);
+
+	const inbox = inboxes.get(room.email.inbox);
+
+	if (!inbox) {
+		return sendErrorReplyMessage(`Email inbox ${ room.email.inbox } not found or disabled.`, {
+			msgId: message._id,
+			sender: message.u.username,
+			rid: room._id,
+		});
+	}
+
+	const file = Uploads.findOneById(message.file._id);
+
+	FileUpload.getBuffer(file, (_err?: Error, buffer?: Buffer) => {
+		sendEmail(inbox, {
+			to: room.email.replyTo,
+			subject: room.email.subject,
+			text: message.attachments[0].description || '',
+			attachments: [{
+				content: buffer,
+				contentType: file.type,
+				filename: file.name,
+			}],
+			inReplyTo: room.email.thread,
+			references: [
+				room.email.thread,
+			],
+		},
+		{
+			msgId: message._id,
+			sender: message.u.username,
+			rid: message.rid,
+		});
+	});
+
+	Messages.update({ _id: message._id }, {
+		$set: {
+			blocks: [{
+				type: 'context',
+				elements: [{
+					type: 'mrkdwn',
+					text: `**${ t('To') }:** ${ room.email.replyTo }\n**${ t('Subject') }:** ${ room.email.subject }`,
+				}],
+			}],
+		},
+		$pull: {
+			attachments: { 'actions.0.type': 'button' },
+		},
+	});
+}, {
+	description: 'Send attachment as email',
+	params: 'msg_id',
+});
+
+callbacks.add('beforeSaveMessage', function(message: any, room: any) {
+	if (!room?.email?.inbox) {
+		return message;
+	}
+
+	if (message.file) {
+		message.attachments.push({
+			actions: [{
+				type: 'button',
+				text: t('Send_via_Email_as_attachment'),
+				msg: `/sendEmailAttachment ${ message._id }`,
+				msg_in_chat_window: true,
+				msg_processing_type: 'sendMessage',
+			}],
+		});
+
+		return message;
+	}
+
+	const { msg } = message;
+
+	// Try to identify a quote in a livechat room
+	const match = msg.match(livechatQuoteRegExp);
+	if (!match) {
+		return message;
+	}
+
+	const inbox = inboxes.get(room.email.inbox);
+
+	if (!inbox) {
+		sendErrorReplyMessage(`Email inbox ${ room.email.inbox } not found or disabled.`, {
+			msgId: message._id,
+			sender: message.u.username,
+			rid: room._id,
+		});
+
+		return message;
+	}
+
+	if (!inbox) {
+		return message;
+	}
+
+	const replyToMessage = Messages.findOneById(match.groups.id);
+
+	if (!replyToMessage?.email?.messageId) {
+		return message;
+	}
+
+	sendEmail(inbox, {
+		text: match.groups.text,
+		inReplyTo: replyToMessage.email.messageId,
+		references: [
+			...replyToMessage.email.references ?? [],
+			replyToMessage.email.messageId,
+		],
+		to: room.email.replyTo,
+		subject: room.email.subject,
+	},
+	{
+		msgId: message._id,
+		sender: message.u.username,
+		rid: room._id,
+	});
+
+	message.msg = match.groups.text;
+
+	message.groupable = false;
+
+	message.blocks = [{
+		type: 'context',
+		elements: [{
+			type: 'mrkdwn',
+			text: `**${ t('To') }:** ${ room.email.replyTo }\n**${ t('Subject') }:** ${ room.email.subject }`,
+		}],
+	}, {
+		type: 'section',
+		text: {
+			type: 'mrkdwn',
+			text: message.msg,
+		},
+	}, {
+		type: 'section',
+		text: {
+			type: 'mrkdwn',
+			text: `> ---\n${ replyToMessage.msg.replace(/^/gm, '> ') }`,
+		},
+	}];
+
+	delete message.urls;
+
+	return message;
+}, callbacks.priority.LOW, 'ReplyEmail');
+
+export async function sendTestEmailToInbox(emailInboxRecord: IEmailInbox, user: IUser): Promise<void> {
+	const inbox = inboxes.get(emailInboxRecord.email);
+
+	if (!inbox) {
+		throw new Error('inbox-not-found');
+	}
+
+	const address = user.emails?.find((email) => email.verified)?.address;
+
+	if (!address) {
+		throw new Error('user-without-verified-email');
+	}
+
+	console.log(`Sending testing email to ${ address }`);
+	sendEmail(inbox, {
+		to: address,
+		subject: 'Test of inbox configuration',
+		text: 'Test of inbox configuration successful',
+	});
+}
diff --git a/server/features/EmailInbox/index.ts b/server/features/EmailInbox/index.ts
new file mode 100644
index 00000000000..dfe6fde6be9
--- /dev/null
+++ b/server/features/EmailInbox/index.ts
@@ -0,0 +1,2 @@
+import './EmailInbox_Incoming';
+import './EmailInbox_Outgoing';
diff --git a/server/main.js b/server/main.js
index d00699b9735..97a32898ed2 100644
--- a/server/main.js
+++ b/server/main.js
@@ -75,3 +75,5 @@ import './publications/spotlight';
 import './publications/subscription';
 import './routes/avatar';
 import './stream/streamBroadcast';
+
+import './features/EmailInbox/index';
diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts
index 4e5e7fac482..ca202fc85ee 100644
--- a/server/modules/watchers/watchers.module.ts
+++ b/server/modules/watchers/watchers.module.ts
@@ -30,6 +30,8 @@ import { ILivechatDepartmentAgents } from '../../../definition/ILivechatDepartme
 import { IIntegration } from '../../../definition/IIntegration';
 import { IntegrationsRaw } from '../../../app/models/server/raw/Integrations';
 import { EventSignatures } from '../../sdk/lib/Events';
+import { IEmailInbox } from '../../../definition/IEmailInbox';
+import { EmailInboxRaw } from '../../../app/models/server/raw/EmailInbox';
 
 interface IModelsParam {
 	Subscriptions: SubscriptionsRaw;
@@ -46,6 +48,7 @@ interface IModelsParam {
 	InstanceStatus: InstanceStatusRaw;
 	IntegrationHistory: IntegrationHistoryRaw;
 	Integrations: IntegrationsRaw;
+	EmailInbox: EmailInboxRaw;
 }
 
 interface IChange<T> {
@@ -77,6 +80,7 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback,
 		InstanceStatus,
 		IntegrationHistory,
 		Integrations,
+		EmailInbox,
 	} = models;
 
 	watch<IMessage>(Messages, async ({ clientAction, id, data }) => {
@@ -334,4 +338,18 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback,
 
 		broadcast('watch.integrations', { clientAction, data, id });
 	});
+
+	watch<IEmailInbox>(EmailInbox, async ({ clientAction, id, data }) => {
+		if (clientAction === 'removed') {
+			broadcast('watch.emailInbox', { clientAction, id, data: { _id: id } });
+			return;
+		}
+
+		data = data ?? await EmailInbox.findOneById(id);
+		if (!data) {
+			return;
+		}
+
+		broadcast('watch.emailInbox', { clientAction, data, id });
+	});
 }
diff --git a/server/sdk/lib/Events.ts b/server/sdk/lib/Events.ts
index 5b81503f3e3..62d1a39145a 100644
--- a/server/sdk/lib/Events.ts
+++ b/server/sdk/lib/Events.ts
@@ -14,6 +14,7 @@ import { IInstanceStatus } from '../../../definition/IInstanceStatus';
 import { IIntegrationHistory } from '../../../definition/IIntegrationHistory';
 import { ILivechatDepartmentAgents } from '../../../definition/ILivechatDepartmentAgents';
 import { IIntegration } from '../../../definition/IIntegration';
+import { IEmailInbox } from '../../../definition/IEmailInbox';
 
 export type EventSignatures = {
 	'emoji.deleteCustom'(emoji: IEmoji): void;
@@ -48,5 +49,6 @@ export type EventSignatures = {
 	'watch.instanceStatus'(data: { clientAction: string; data?: Partial<IInstanceStatus>; diff?: Record<string, any>; id: string }): void;
 	'watch.integrationHistory'(data: { clientAction: string; data: Partial<IIntegrationHistory>; diff?: Record<string, any>; id: string }): void;
 	'watch.integrations'(data: { clientAction: string; data: Partial<IIntegration>; id: string }): void;
+	'watch.emailInbox'(data: { clientAction: string; data: Partial<IEmailInbox>; id: string }): void;
 	'watch.livechatDepartmentAgents'(data: { clientAction: string; data: Partial<ILivechatDepartmentAgents>; diff?: Record<string, any>; id: string }): void;
 }
diff --git a/server/services/meteor/service.ts b/server/services/meteor/service.ts
index 8f3a80320a7..495c19994dc 100644
--- a/server/services/meteor/service.ts
+++ b/server/services/meteor/service.ts
@@ -19,6 +19,7 @@ import { matrixBroadCastActions } from '../../stream/streamBroadcast';
 import { integrations } from '../../../app/integrations/server/lib/triggerHandler';
 import { ListenersModule, minimongoChangeMap } from '../../modules/listeners/listeners.module';
 import notifications from '../../../app/notifications/server/lib/Notifications';
+import { configureEmailInboxes } from '../../features/EmailInbox/EmailInbox';
 
 
 const autoUpdateRecords = new Map<string, AutoUpdateRecord>();
@@ -232,6 +233,10 @@ export class MeteorService extends ServiceClass implements IMeteor {
 					break;
 			}
 		});
+
+		this.onEvent('watch.emailInbox', async () => {
+			configureEmailInboxes();
+		});
 	}
 
 	async getLastAutoUpdateClientVersions(): Promise<AutoUpdateRecord[]> {
-- 
GitLab