[NEW] Email Inboxes for Omnichannel (#20101)

pull/20318/head^2
Rafael Ferreira 5 years ago committed by GitHub
parent 64d77c99b5
commit 2f90da3f17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/api/server/index.js
  2. 79
      app/api/server/lib/emailInbox.js
  3. 131
      app/api/server/v1/email-inbox.js
  4. 1
      app/authorization/server/startup.js
  5. 133
      app/lib/server/lib/interceptDirectReplyEmails.js
  6. 2
      app/livechat/client/views/app/tabbar/visitorInfo.html
  7. 3
      app/livechat/client/views/app/tabbar/visitorInfo.js
  8. 2
      app/livechat/server/business-hour/AbstractBusinessHour.ts
  9. 48
      app/livechat/server/lib/QueueManager.js
  10. 4
      app/mailer/server/api.js
  11. 2
      app/models/server/index.js
  12. 27
      app/models/server/models/EmailInbox.js
  13. 43
      app/models/server/models/LivechatRooms.js
  14. 55
      app/models/server/raw/BaseRaw.ts
  15. 6
      app/models/server/raw/EmailInbox.ts
  16. 14
      app/models/server/raw/LivechatBusinessHours.ts
  17. 5
      app/models/server/raw/index.ts
  18. 21
      app/ui-message/client/messageBox/messageBox.js
  19. 25
      app/ui/client/views/app/lib/getCommonRoomEvents.js
  20. 3
      app/utils/lib/slashCommand.d.ts
  21. 4
      client/components/AutoCompleteDepartment.js
  22. 7
      client/components/Message/Attachments/ActionAttachtment.tsx
  23. 8
      client/components/Sidebar.js
  24. 361
      client/views/admin/emailInbox/EmailInboxForm.js
  25. 42
      client/views/admin/emailInbox/EmailInboxPage.js
  26. 17
      client/views/admin/emailInbox/EmailInboxRoute.js
  27. 73
      client/views/admin/emailInbox/EmailInboxTable.js
  28. 11
      client/views/admin/emailInbox/Skeleton.js
  29. 5
      client/views/admin/routes.js
  30. 6
      client/views/admin/sidebarItems.js
  31. 2
      client/views/omnichannel/currentChats/CurrentChatsPage.js
  32. 29
      definition/IEmailInbox.ts
  33. 3
      ee/server/services/stream-hub/StreamHub.ts
  34. 195
      package-lock.json
  35. 7
      package.json
  36. 24
      packages/rocketchat-i18n/i18n/en.i18n.json
  37. 20
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  38. 146
      server/email/IMAPInterceptor.ts
  39. 69
      server/features/EmailInbox/EmailInbox.ts
  40. 203
      server/features/EmailInbox/EmailInbox_Incoming.ts
  41. 238
      server/features/EmailInbox/EmailInbox_Outgoing.ts
  42. 2
      server/features/EmailInbox/index.ts
  43. 2
      server/main.js
  44. 18
      server/modules/watchers/watchers.module.ts
  45. 2
      server/sdk/lib/Events.ts
  46. 5
      server/services/meteor/service.ts

@ -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';

@ -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 });
}

@ -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 });
},
});

@ -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'] },

@ -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)));
}
}

@ -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}}

@ -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() {

@ -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);

@ -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;
},
};

@ -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,
});

@ -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,
};

@ -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();

@ -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);

@ -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);

@ -0,0 +1,6 @@
import { BaseRaw } from './BaseRaw';
import { IEmailInbox } from '../../../../definition/IEmailInbox';
export class EmailInboxRaw extends BaseRaw<IEmailInbox> {
//
}

@ -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({

@ -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) => {

@ -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,

@ -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;

@ -0,0 +1,3 @@
export declare const slashCommand: {
add(command: string, callback: Function, options: object /* , result, providesPreview = false, previewer, previewCallback*/): void;
};

@ -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}

@ -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>;

@ -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)}
/>);
};

@ -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>;
}

@ -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;

@ -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;

@ -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;

@ -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>;

@ -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',

@ -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']),
},
]);

@ -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>

@ -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;
}

@ -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) => {

195
package-lock.json generated

@ -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",

@ -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",

@ -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.",

@ -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",

@ -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 }`);
});
}
});
}
}

@ -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();
});

@ -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);
});
}

@ -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',
});
}

@ -0,0 +1,2 @@
import './EmailInbox_Incoming';
import './EmailInbox_Outgoing';

@ -75,3 +75,5 @@ import './publications/spotlight';
import './publications/subscription';
import './routes/avatar';
import './stream/streamBroadcast';
import './features/EmailInbox/index';

@ -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 });
});
}

@ -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;
}

@ -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[]> {

Loading…
Cancel
Save