diff --git a/app/api/server/index.js b/app/api/server/index.js
index 568664ede60f0..466b92e668fbe 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 0000000000000..43d7a0e8f64ff
--- /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 0000000000000..e7452fc5ffe1e
--- /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 e769cc2b500d9..7d5efbdca1679 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 0af20d2e2d196..0ef5361b66d6c 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 a4624bef2a704..c46ed0b1ec51d 100644
--- a/app/livechat/client/views/app/tabbar/visitorInfo.html
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.html
@@ -39,6 +39,8 @@
{{_ "Conversation"}}
{{#with room}}
{{#if servedBy}}{{_ "Agent"}} : {{servedBy.username}} {{/if}}
+ {{#if email}}{{_ "Email_Inbox"}} : {{email.inbox}} {{/if}}
+ {{#if email}}{{_ "Email_subject"}} : {{email.subject}} {{/if}}
{{#if facebook}} {{_ "Facebook_Page"}}: {{facebook.page.name}} {{/if}}
{{#if sms}} {{_ "SMS_Enabled"}} {{/if}}
{{#if topic}}{{_ "Topic"}} : {{{markdown topic}}} {{/if}}
diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js
index 46e11e7f52f97..f061207a35af6 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 82f9bbc490595..32093d843eea1 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 46ed51b7a14e6..33cf8dcf45eef 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 9b93aa4abd272..395af628b7d19 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 fecf4de953b85..efcd3790302ff 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 0000000000000..490628be33837
--- /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 a763bbee23ddb..a57b5e9f3581d 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 f602de361df0a..f26ef02922b44 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 = 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> // discriminated unions
+ : never;
+
+// [extracted from @types/mongo]
+type ExtractIdType = TSchema extends { _id: infer U } // user has defined a type for _id
+ ? {} extends U
+ ? Exclude
+ : unknown extends U
+ ? ObjectId
+ : U
+ : ObjectId;
+
+type ModelOptionalId = EnhancedOmit & { _id?: ExtractIdType };
interface ITrash {
__collection__: string;
@@ -70,6 +105,24 @@ export class BaseRaw implements IBaseRaw {
return this.col.update(filter, update, options);
}
+ updateOne(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise {
+ return this.col.updateOne(filter, update, options);
+ }
+
+ updateMany(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateManyOptions): Promise {
+ return this.col.updateMany(filter, update, options);
+ }
+
+ insertOne(doc: ModelOptionalId, options?: CollectionInsertOneOptions): Promise>> {
+ 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, options);
+ }
+
removeById(_id: string): Promise {
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 0000000000000..1d8d008242fa8
--- /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 {
+ //
+}
diff --git a/app/models/server/raw/LivechatBusinessHours.ts b/app/models/server/raw/LivechatBusinessHours.ts
index 09993b1697f5f..b48e53a841569 100644
--- a/app/models/server/raw/LivechatBusinessHours.ts
+++ b/app/models/server/raw/LivechatBusinessHours.ts
@@ -57,20 +57,6 @@ export class LivechatBusinessHoursRaw extends BaseRaw {
});
}
- async updateOne(_id: string, data: Omit): Promise {
- 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): Promise {
return this.col.updateOne({
diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts
index 7c2a4d92ab58f..243b627d5daa1 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 86c3aff9eee98..46856eeecea10 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 d4f5b3bfbacc4..3a28c14094aa4 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 0000000000000..46ccc3ed68ce1
--- /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 2e2c13e8b7f7f..acc8781d41a30 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 = ({ actions }) => {
- 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 ? : text;
if (url) {
return {content} ;
}
- return {content} ;
+ return {content} ;
})} ;
diff --git a/client/components/Sidebar.js b/client/components/Sidebar.js
index 05af4600013b3..aaafe865b9cbe 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 }) =>
;
-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
{icon && }
- {label}
+ {label} {tag && {tag} }
;
};
@@ -79,6 +79,7 @@ const ItemsAssembler = ({ items, currentPath }) => {
icon,
permissionGranted,
pathGroup,
+ tag,
}) => {
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 0000000000000..1dbaa0a1333dc
--- /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 ;
+ }
+
+ if (error || !data) {
+ return {t('EmailInbox_not_found')} ;
+ }
+
+ return ;
+}
+
+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( setModal()}
+ >{t('You_will_not_be_able_to_recover_email_inbox')} );
+ });
+
+ 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
+
+
+
+
+
+
+ {t('Active')}
+
+
+
+
+ {t('Name')}*
+
+
+
+
+
+ {t('Email')}*
+
+
+
+
+ {t(emailError)}
+
+
+
+ {t('Description')}
+
+
+
+
+
+ {t('Sender_Info')}
+
+
+
+
+ {t('Will_Appear_In_From')}
+
+
+
+ {t('Department')}
+
+
+
+
+ {t('Only_Members_Selected_Department_Can_View_Channel')}
+
+
+
+
+
+
+
+ {t('Server')}*
+
+
+
+
+
+ {t('Port')}*
+
+
+
+
+
+ {t('Username')}*
+
+
+
+
+
+ {t('Password')}*
+
+
+
+
+
+
+ {t('Connect_SSL_TLS')}
+
+
+
+
+
+
+
+
+ {t('Server')}*
+
+
+
+
+
+ {t('Port')}*
+
+
+
+
+
+ {t('Username')}*
+
+
+
+
+
+ {t('Password')}*
+
+
+
+
+
+
+ {t('Connect_SSL_TLS')}
+
+
+
+
+
+
+
+
+ {t('Cancel')}
+ {t('Save')}
+
+
+
+
+
+ {id && {t('Delete')} }
+
+
+
+
+
+
+ ;
+}
diff --git a/client/views/admin/emailInbox/EmailInboxPage.js b/client/views/admin/emailInbox/EmailInboxPage.js
new file mode 100644
index 0000000000000..805952f95d9b1
--- /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
+
+
+ {context && emailInboxRoute.push({})}>
+ {t('Back')}
+ }
+ {!context &&
+ {t('New_Email_Inbox')}
+ }
+
+
+ {!context && }
+ {context === 'new' && }
+ {context === 'edit' && }
+
+
+ ;
+}
+
+export default EmailInboxPage;
diff --git a/client/views/admin/emailInbox/EmailInboxRoute.js b/client/views/admin/emailInbox/EmailInboxRoute.js
new file mode 100644
index 0000000000000..fc60932361a8d
--- /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 ;
+ }
+
+ return ;
+}
+
+export default EmailInboxRoute;
diff --git a/client/views/admin/emailInbox/EmailInboxTable.js b/client/views/admin/emailInbox/EmailInboxTable.js
new file mode 100644
index 0000000000000..e4e5cc57958af
--- /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
+ e.preventDefault() & e.stopPropagation() & sendTest() & dispatchToastMessage({ type: 'success', message: t('Email_sent') })}>
+
+
+ ;
+}
+
+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(() => [
+ {t('Name')} ,
+ {t('Email')} ,
+ {t('Active')} ,
+ ,
+ ].filter(Boolean), [sort, t]);
+
+ const { value: data } = useEndpointData('email-inbox.list', query);
+
+ const renderRow = useCallback(({ _id, name, email, active }) =>
+ {name}
+ {email}
+ {active ? t('Yes') : t('No')}
+
+ , [onClick, t]);
+
+ return ;
+}
+
+export default EmailInboxTable;
diff --git a/client/views/admin/emailInbox/Skeleton.js b/client/views/admin/emailInbox/Skeleton.js
new file mode 100644
index 0000000000000..2fc5e9097c149
--- /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) =>
+
+
+
+
+
+
+ ;
diff --git a/client/views/admin/routes.js b/client/views/admin/routes.js
index 1f00ff7370ab9..1d560236cb7e3 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 95d9b36ba0410..a4e904617255b 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 2744b029883e7..b915a09d95908 100644
--- a/client/views/omnichannel/currentChats/CurrentChatsPage.js
+++ b/client/views/omnichannel/currentChats/CurrentChatsPage.js
@@ -145,7 +145,7 @@ const FilterByText = ({ setFilter, reload, ...props }) => {
{t('Department')}
-
+
{t('Status')}
diff --git a/definition/IEmailInbox.ts b/definition/IEmailInbox.ts
new file mode 100644
index 0000000000000..2f0ff390297a8
--- /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 5ddf3c4b5cd3b..b559e43a7f750 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 5c490e4c6b7f8..8032c38017724 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 9a7cf4e1f476b..a803d9ddda735 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 0d3caed480606..59aa6d56837e9 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: [Site_Name] and [Site_URL] for the Application Name and URL respectively. ",
"Email_from": "From",
"Email_Header_Description": "You may use the following placeholders: [Site_Name] and [Site_URL] for the Application Name and URL respectively. ",
+ "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 JoyPixels ",
@@ -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 0df150be6f169..4d9d089ddb467 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: [name], [fname], [lname] para o nome do usuário completo, primeiro nome ou sobrenome, respectivamente. [email] para e-mail do usuário. [Senha] para a senha do usuário. [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. ",
"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 %s por %s ",
"Created_at_s_by_s_triggered_by_s": "Criado em %s por %s desencadeado por %s ",
@@ -1246,6 +1252,7 @@
"Email_Footer_Description": "Você pode usar os seguintes espaços reservados: [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. ",
"Email_from": "De",
"Email_Header_Description": "Você pode usar os seguintes espaços reservados: [Site_Name] e [Site_URL] para o nome do aplicativo e URL, respectivamente. ",
+ "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 0000000000000..a8d631ae9160b
--- /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 0000000000000..21ffbfdffbe73
--- /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();
+
+export async function configureEmailInboxes(): Promise {
+ 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 0000000000000..d7927f06db304
--- /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 {
+ 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 {
+ 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(/