Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
1ce6de9
start email as channel, create new item on admin sidebar and email ch…
rafaelblink Jan 5, 2021
f8f0a27
new collection "rocketchat_email_channels" and permissions.
rafaelblink Jan 5, 2021
3535a45
Implement findOne and findAll email channels
rafaelblink Jan 6, 2021
d8ecf38
implement POST for email channel api and list page with data.
rafaelblink Jan 6, 2021
f944ade
missing tooltip of send email test and translation
rafaelblink Jan 6, 2021
5e1e21e
refactor on AutoCompleteDepartment to accept none label.
rafaelblink Jan 6, 2021
cdc0017
fix margin left from sidebar left tag.
rafaelblink Jan 7, 2021
6533a25
implements createdBy and refactor required fields from POST Api.
rafaelblink Jan 7, 2021
d6c4a6c
Create new email channel and showing createdBy on grid
rafaelblink Jan 7, 2021
2795611
email channel edit form with API implementation.
rafaelblink Jan 7, 2021
c1bda33
removing unused stuff
rafaelblink Jan 7, 2021
062177f
Renaming file and refactoring variable names.
rafaelblink Jan 7, 2021
26fc7e1
removing required fields from API.
rafaelblink Jan 7, 2021
cf04646
Auxiliary file only user raw models, change to the api use model.
rafaelblink Jan 8, 2021
69649ec
missing translations pt-br and some new values in english
rafaelblink Jan 8, 2021
39ff423
error translations and hasUnsavedChanges check on edit.
rafaelblink Jan 8, 2021
48077ec
t('All') as fallback when label is not defined on Department Auto Com…
rafaelblink Jan 8, 2021
8eba0d0
Implements remove button and api method.
rafaelblink Jan 8, 2021
003800d
renaming Email Channel to Email Inbox.
rafaelblink Jan 8, 2021
4d85896
Refactor POST API
rafaelblink Jan 8, 2021
4f030e6
Email Inbox to Email Inboxes
rafaelblink Jan 8, 2021
68b3a01
Fix LGTM, async methods with no "await".
rafaelblink Jan 8, 2021
d0ad4b0
unecessary promise await inside async method
rafaelblink Jan 8, 2021
f1b0174
refactor api method and variable names
rafaelblink Jan 8, 2021
17a5f65
translations changes and autocomplete refactor.
rafaelblink Jan 8, 2021
1b29a11
email validation with icon on field
rafaelblink Jan 8, 2021
309f923
check existent email and when is edit, check different from the same.
rafaelblink Jan 8, 2021
45efeb1
Merge branch 'develop' into omnichannel/email-as-channel
rodrigok Jan 12, 2021
c581a89
refactor email checker and some validations on server side.
rafaelblink Jan 12, 2021
7109ad6
Merge remote-tracking branch 'origin/omnichannel/email-as-channel' in…
rafaelblink Jan 12, 2021
195c3f4
remove unused icon import
rafaelblink Jan 12, 2021
dc40781
split post from get url API
rafaelblink Jan 12, 2021
a7827a3
Merge branch 'develop' into omnichannel/email-as-channel
renatobecker Jan 12, 2021
6da4c1d
Merge branch 'develop' into omnichannel/email-as-channel
rafaelblink Jan 13, 2021
349d6c5
Merge remote-tracking branch 'origin/develop' into omnichannel/email-…
rodrigok Jan 13, 2021
cf1ccb8
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
rodrigok Jan 13, 2021
5eecfa8
Merge remote-tracking branch 'origin/develop' into omnichannel/email-…
rodrigok Jan 14, 2021
78d9c7d
Merge remote-tracking branch 'origin/develop' into omnichannel/email-…
rodrigok Jan 16, 2021
070a302
Add insertOne in BaseRaw (from Sampaio’s commit)
rodrigok Jan 16, 2021
53402ac
Convert EmailInboxRaw to TS
rodrigok Jan 16, 2021
68856b2
Extract IMAPInterceptor into a module
rodrigok Jan 16, 2021
12db03c
Initial implementation of email fetching (missing update config while…
rodrigok Jan 16, 2021
bb6faa1
Pass emails to omni routing creating messages and grouping correctly
rodrigok Jan 16, 2021
e87175b
Move things around
rodrigok Jan 16, 2021
567c016
Intercept message quotes and reply as email
rodrigok Jan 17, 2021
218d444
Usei UI Kit to render the email with more info
rodrigok Jan 17, 2021
10c0dcf
Update inboxes config listening to database
rodrigok Jan 17, 2021
f81e645
Fix types
rodrigok Jan 17, 2021
b41f5e6
Handle sender info
rodrigok Jan 17, 2021
53cbdf3
Handle departments
rodrigok Jan 17, 2021
b555e72
Implement button to test email config
rodrigok Jan 17, 2021
22e3514
Receive attachments from email and link to the message
rodrigok Jan 18, 2021
4c03f6b
attachment actionButton with reply quoted event.
rafaelblink Jan 19, 2021
9bd525d
Add msgId as optional field.
renatobecker Jan 19, 2021
7ca0b53
Show Email Inbox and Subject on visitor page and hide Transcript button.
rafaelblink Jan 19, 2021
c01475a
Merge branch 'develop' into omnichannel/email-as-channel
rafaelblink Jan 19, 2021
bfe6ce1
Allow to send attachments back via email
rodrigok Jan 20, 2021
8e401ac
Merge remote-tracking branch 'origin/develop' into omnichannel/email-…
rodrigok Jan 20, 2021
cb119de
Merge branch 'develop' into omnichannel/email-as-channel
renatobecker Jan 20, 2021
57546b0
Fix LGTM(unnecessary parameter)
renatobecker Jan 20, 2021
2ba95f9
Fix condition missing when replying emails.
renatobecker Jan 20, 2021
c77cd07
Improve code organization
rodrigok Jan 20, 2021
1762e07
unarchive closed chats.
renatobecker Jan 20, 2021
9cf2204
Merge branch 'develop' into omnichannel/email-as-channel
renatobecker Jan 20, 2021
26463ba
Unarchive closed email channels.
renatobecker Jan 20, 2021
0405048
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
renatobecker Jan 20, 2021
d8fc7d4
Improve UI of sent emails and attachments
rodrigok Jan 20, 2021
ad193a3
Remove unnecessary code.
renatobecker Jan 20, 2021
cc47eed
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
rodrigok Jan 20, 2021
439b033
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
renatobecker Jan 20, 2021
addc9cc
Fix import.
renatobecker Jan 20, 2021
2dbe320
Check if message came from email before replying.
renatobecker Jan 20, 2021
26df865
Remove unnecessary file.
renatobecker Jan 20, 2021
710acc5
Add translations.
renatobecker Jan 20, 2021
2d4262e
Fix issue not allowing quote after taking the room
rodrigok Jan 21, 2021
5c89436
Merge branch 'develop' into omnichannel/email-as-channel
renatobecker Jan 21, 2021
b589041
Rename "SSL TLS" to "Secure"
rafaelblink Jan 21, 2021
357b437
Merge remote-tracking branch 'origin/omnichannel/email-as-channel' in…
rafaelblink Jan 21, 2021
cf6cd3f
Handle email reply errors.
renatobecker Jan 21, 2021
19c2c31
show "active" instead "created by" on Email Inbox List.
rafaelblink Jan 21, 2021
0328e02
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
renatobecker Jan 21, 2021
bae54d2
Remove all try catch unnecessary on Email Inboxes API endpoints and s…
rafaelblink Jan 21, 2021
de5fdce
Merge branch 'omnichannel/email-as-channel' of https://github.com/Roc…
renatobecker Jan 21, 2021
1e25718
Change port to number type and some code enhancements
rafaelblink Jan 21, 2021
2a1a6a2
Merge remote-tracking branch 'origin/omnichannel/email-as-channel' in…
rafaelblink Jan 21, 2021
3206527
Handle promisse error when service is offline.
renatobecker Jan 21, 2021
1b2348e
Fix wrong typecast.
renatobecker Jan 21, 2021
925f3ba
Fix error when starting a new chat with attachments.
renatobecker Jan 21, 2021
1a00a3b
Merge branch 'develop' into omnichannel/email-as-channel
renatobecker Jan 21, 2021
cfd257c
Merge branch 'develop' into omnichannel/email-as-channel
rafaelblink Jan 22, 2021
4b330d6
Default ports (IMAP and SMTP) for new email inbox.
rafaelblink Jan 22, 2021
cdc0c39
Remove duplicated translation PT-BR
rafaelblink Jan 22, 2021
5cb4be2
TAPi18n not working properly on server side.
rafaelblink Jan 22, 2021
a8f8a72
Message header translations missing
rafaelblink Jan 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
79 changes: 79 additions & 0 deletions app/api/server/lib/emailInbox.js
Original file line number Diff line number Diff line change
@@ -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 });
}
131 changes: 131 additions & 0 deletions app/api/server/v1/email-inbox.js
Original file line number Diff line number Diff line change
@@ -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 });
},
});
1 change: 1 addition & 0 deletions app/authorization/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
Expand Down
133 changes: 9 additions & 124 deletions app/lib/server/lib/interceptDirectReplyEmails.js
Original file line number Diff line number Diff line change
@@ -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)));
}
}

Expand Down
2 changes: 2 additions & 0 deletions app/livechat/client/views/app/tabbar/visitorInfo.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ <h3>{{_ "Conversation"}}</h3>
<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}}
Expand Down
3 changes: 2 additions & 1 deletion app/livechat/client/views/app/tabbar/visitorInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading