diff --git a/app/api/server/index.js b/app/api/server/index.js index 568664ede60f0..466b92e668fbe 100644 --- a/app/api/server/index.js +++ b/app/api/server/index.js @@ -38,5 +38,6 @@ import './v1/oauthapps'; import './v1/custom-sounds'; import './v1/custom-user-status'; import './v1/instances'; +import './v1/email-inbox'; export { API, APIClass, defaultRateLimiterOptions } from './api'; diff --git a/app/api/server/lib/emailInbox.js b/app/api/server/lib/emailInbox.js new file mode 100644 index 0000000000000..43d7a0e8f64ff --- /dev/null +++ b/app/api/server/lib/emailInbox.js @@ -0,0 +1,79 @@ +import { EmailInbox } from '../../../models/server/raw'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Users } from '../../../models'; + +export async function findEmailInboxes({ userId, query = {}, pagination: { offset, count, sort } }) { + if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + const cursor = EmailInbox.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const emailInboxes = await cursor.toArray(); + + return { + emailInboxes, + count: emailInboxes.length, + offset, + total, + }; +} + +export async function findOneEmailInbox({ userId, _id }) { + if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + return EmailInbox.findOneById(_id); +} + +export async function insertOneOrUpdateEmailInbox(userId, emailInboxParams) { + const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; + + if (!_id) { + emailInboxParams._createdAt = new Date(); + emailInboxParams._updatedAt = new Date(); + emailInboxParams._createdBy = Users.findOne(userId, { fields: { username: 1 } }); + return EmailInbox.insertOne(emailInboxParams); + } + + const emailInbox = await findOneEmailInbox({ userId, id: _id }); + + if (!emailInbox) { + throw new Error('error-invalid-email-inbox'); + } + + const updateEmailInbox = { + $set: { + active, + name, + email, + description, + senderInfo, + smtp, + imap, + _updatedAt: new Date(), + }, + }; + + if (department === 'All') { + updateEmailInbox.$unset = { + department: 1, + }; + } else { + updateEmailInbox.$set.department = department; + } + + return EmailInbox.updateOne({ _id }, updateEmailInbox); +} + +export async function findOneEmailInboxByEmail({ userId, email }) { + if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + return EmailInbox.findOne({ email }); +} diff --git a/app/api/server/v1/email-inbox.js b/app/api/server/v1/email-inbox.js new file mode 100644 index 0000000000000..e7452fc5ffe1e --- /dev/null +++ b/app/api/server/v1/email-inbox.js @@ -0,0 +1,131 @@ +import { check, Match } from 'meteor/check'; + +import { API } from '../api'; +import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox'; +import { hasPermission } from '../../../authorization/server/functions/hasPermission'; +import { EmailInbox } from '../../../models'; +import Users from '../../../models/server/models/Users'; +import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; + +API.v1.addRoute('email-inbox.list', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + const emailInboxes = Promise.await(findEmailInboxes({ userId: this.userId, query, pagination: { offset, count, sort } })); + + return API.v1.success(emailInboxes); + }, +}); + +API.v1.addRoute('email-inbox', { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + check(this.bodyParams, { + _id: Match.Maybe(String), + name: String, + email: String, + active: Boolean, + description: Match.Maybe(String), + senderInfo: Match.Maybe(String), + department: Match.Maybe(String), + smtp: Match.ObjectIncluding({ + password: String, + port: Number, + secure: Boolean, + server: String, + username: String, + }), + imap: Match.ObjectIncluding({ + password: String, + port: Number, + secure: Boolean, + server: String, + username: String, + }), + }); + + const emailInboxParams = this.bodyParams; + + const { _id } = emailInboxParams; + + Promise.await(insertOneOrUpdateEmailInbox(this.userId, emailInboxParams)); + + return API.v1.success({ _id }); + }, +}); + +API.v1.addRoute('email-inbox/:_id', { authRequired: true }, { + get() { + check(this.urlParams, { + _id: String, + }); + + const { _id } = this.urlParams; + if (!_id) { throw new Error('error-invalid-param'); } + const emailInboxes = Promise.await(findOneEmailInbox({ userId: this.userId, _id })); + + return API.v1.success(emailInboxes); + }, + delete() { + if (!hasPermission(this.userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + check(this.urlParams, { + _id: String, + }); + + const { _id } = this.urlParams; + if (!_id) { throw new Error('error-invalid-param'); } + + const emailInboxes = EmailInbox.findOneById(_id); + + if (!emailInboxes) { + return API.v1.notFound(); + } + EmailInbox.removeById(_id); + return API.v1.success({ _id }); + }, +}); + +API.v1.addRoute('email-inbox.search', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + check(this.queryParams, { + email: String, + }); + + const { email } = this.queryParams; + const emailInbox = Promise.await(EmailInbox.findOne({ email })); + + return API.v1.success({ emailInbox }); + }, +}); + +API.v1.addRoute('email-inbox.send-test/:_id', { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'manage-email-inbox')) { + throw new Error('error-not-allowed'); + } + check(this.urlParams, { + _id: String, + }); + + const { _id } = this.urlParams; + if (!_id) { throw new Error('error-invalid-param'); } + const emailInbox = Promise.await(findOneEmailInbox({ userId: this.userId, _id })); + + if (!emailInbox) { + return API.v1.notFound(); + } + + const user = Users.findOneById(this.userId); + + Promise.await(sendTestEmailToInbox(emailInbox, user)); + + return API.v1.success({ _id }); + }, +}); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index e769cc2b500d9..7d5efbdca1679 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -52,6 +52,7 @@ Meteor.startup(function() { { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, { _id: 'manage-assets', roles: ['admin'] }, + { _id: 'manage-email-inbox', roles: ['admin'] }, { _id: 'manage-emoji', roles: ['admin'] }, { _id: 'manage-user-status', roles: ['admin'] }, { _id: 'manage-outgoing-integrations', roles: ['admin'] }, diff --git a/app/lib/server/lib/interceptDirectReplyEmails.js b/app/lib/server/lib/interceptDirectReplyEmails.js index 0af20d2e2d196..0ef5361b66d6c 100644 --- a/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/app/lib/server/lib/interceptDirectReplyEmails.js @@ -1,144 +1,29 @@ import { Meteor } from 'meteor/meteor'; -import IMAP from 'imap'; import POP3Lib from 'poplib'; import { simpleParser } from 'mailparser'; import { settings } from '../../../settings'; +import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor'; import { processDirectEmail } from '.'; -export class IMAPIntercepter { - constructor() { - this.imap = new IMAP({ +export class IMAPIntercepter extends IMAPInterceptor { + constructor(imapConfig, options = {}) { + imapConfig = { user: settings.get('Direct_Reply_Username'), password: settings.get('Direct_Reply_Password'), host: settings.get('Direct_Reply_Host'), port: settings.get('Direct_Reply_Port'), debug: settings.get('Direct_Reply_Debug') ? console.log : false, tls: !settings.get('Direct_Reply_IgnoreTLS'), - connTimeout: 30000, - keepalive: true, - }); - - this.delete = settings.get('Direct_Reply_Delete'); - - // On successfully connected. - this.imap.on('ready', Meteor.bindEnvironment(() => { - if (this.imap.state !== 'disconnected') { - this.openInbox(Meteor.bindEnvironment((err) => { - if (err) { - throw err; - } - // fetch new emails & wait [IDLE] - this.getEmails(); - - // If new message arrived, fetch them - this.imap.on('mail', Meteor.bindEnvironment(() => { - this.getEmails(); - })); - })); - } else { - console.log('IMAP didnot connected.'); - this.imap.end(); - } - })); - - this.imap.on('error', (err) => { - console.log('Error occurred ...'); - throw err; - }); - } - - openInbox(cb) { - this.imap.openBox('INBOX', false, cb); - } - - start() { - this.imap.connect(); - } - - isActive() { - if (this.imap && this.imap.state && this.imap.state === 'disconnected') { - return false; - } - - return true; - } - - stop(callback = new Function()) { - this.imap.end(); - this.imap.once('end', callback); - } - - restart() { - this.stop(() => { - console.log('Restarting IMAP ....'); - this.start(); - }); - } - - // Fetch all UNSEEN messages and pass them for further processing - getEmails() { - this.imap.search(['UNSEEN'], Meteor.bindEnvironment((err, newEmails) => { - if (err) { - console.log(err); - throw err; - } - - // newEmails => array containing serials of unseen messages - if (newEmails.length > 0) { - const f = this.imap.fetch(newEmails, { - // fetch headers & first body part. - bodies: ['HEADER.FIELDS (FROM TO DATE MESSAGE-ID)', '1'], - struct: true, - markSeen: true, - }); - - f.on('message', Meteor.bindEnvironment((msg, seqno) => { - const email = {}; - - msg.on('body', (stream, info) => { - let headerBuffer = ''; - let bodyBuffer = ''; - - stream.on('data', (chunk) => { - if (info.which === '1') { - bodyBuffer += chunk.toString('utf8'); - } else { - headerBuffer += chunk.toString('utf8'); - } - }); + ...imapConfig, + }; - stream.once('end', () => { - if (info.which === '1') { - email.body = bodyBuffer; - } else { - // parse headers - email.headers = IMAP.parseHeader(headerBuffer); + options.deleteAfterRead = settings.get('Direct_Reply_Delete'); - email.headers.to = email.headers.to[0]; - email.headers.date = email.headers.date[0]; - email.headers.from = email.headers.from[0]; - } - }); - }); + super(imapConfig, options); - // On fetched each message, pass it further - msg.once('end', Meteor.bindEnvironment(() => { - // delete message from inbox - if (this.delete) { - this.imap.seq.addFlags(seqno, 'Deleted', (err) => { - if (err) { console.log(`Mark deleted error: ${ err }`); } - }); - } - processDirectEmail(email); - })); - })); - f.once('error', (err) => { - console.log(`Fetch error: ${ err }`); - }); - } - })); + this.on('email', Meteor.bindEnvironment((email) => processDirectEmail(email))); } } diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html index a4624bef2a704..c46ed0b1ec51d 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.html +++ b/app/livechat/client/views/app/tabbar/visitorInfo.html @@ -39,6 +39,8 @@

{{_ "Conversation"}}