Email feature - wip

This commit is contained in:
Ivan Diaz 2018-12-23 21:44:59 -03:00
parent 7df893f18a
commit 429796aee8
22 changed files with 408 additions and 80 deletions

View File

@ -67,7 +67,7 @@ class AdminPanelSystemPreferences extends React.Component {
<span className="separator" />
<div className="row">
<div className="col-md-6">
<FormField label={i18n('NOREPLY_EMAIL')} fieldProps={{size: 'large'}} name="no-reply-email"/>
<FormField label={i18n('NOREPLY_EMAIL')} fieldProps={{size: 'large'}} name="server-email"/>
<FormField label={i18n('SMTP_USER')} fieldProps={{size: 'large'}} name="smtp-user"/>
</div>
<div className="col-md-6">
@ -173,7 +173,7 @@ class AdminPanelSystemPreferences extends React.Component {
'title': form['title'],
'layout': form['layout'] ? 'full-width' : 'boxed',
'time-zone': form['time-zone'],
'no-reply-email': form['no-reply-email'],
'server-email': form['server-email'],
'smtp-host': form['smtp-host'],
'smtp-port': form['smtp-port'],
'smtp-user': form['smtp-user'],
@ -219,7 +219,7 @@ class AdminPanelSystemPreferences extends React.Component {
'title': result.data['title'],
'layout': (result.data['layout'] == 'full-width') ? 1 : 0,
'time-zone': result.data['time-zone'],
'no-reply-email': result.data['no-reply-email'],
'server-email': result.data['server-email'],
'smtp-host': result.data['smtp-host'],
'smtp-port': result.data['smtp-port'],
'smtp-user': result.data['smtp-user'],

4
client/src/app/install/install-step-5-settings.js Normal file → Executable file
View File

@ -31,7 +31,7 @@ class InstallStep5Settings extends React.Component {
<Form loading={this.state.loading} onSubmit={this.onSubmit.bind(this)} value={this.state.form} onChange={(form) => this.setState({form})}>
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} required/>
<FormField className="install-step-5__attachments-field" name="allow-attachments" label={i18n('ALLOW_FILE_ATTACHMENTS')} field="checkbox" fieldProps={{size: 'large'}}/>
<FormField name="no-reply-email" label={i18n('NOREPLY_EMAIL')} fieldProps={{size: 'large'}}/>
<FormField name="server-email" label={i18n('SERVER_EMAIL')} fieldProps={{size: 'large'}}/>
<div className="install-step-5__smtp-block">
<Header title={i18n('SMTP_SERVER')} description={i18n('SMTP_SERVER_DESCRIPTION')} />
<FormField name="smtp-host" label={i18n('SMTP_SERVER')} fieldProps={{size: 'large'}}/>
@ -132,4 +132,4 @@ export default connect((store) => {
'registration': store.config['registration'],
language: store.config.language
};
})(InstallStep5Settings);
})(InstallStep5Settings);

2
client/src/data/fixtures/system-fixtures.js Normal file → Executable file
View File

@ -14,7 +14,7 @@ module.exports = [
'title': 'Support Center',
'layout': 'boxed',
'time-zone': 3,
'no-reply-email': 'shitr@post.com',
'server-email': 'shitr@post.com',
'smtp-host': 'localhost',
'smtp-port': '7070',
'smtp-user': 'Wesa',

View File

@ -7,7 +7,8 @@
"gabordemooij/redbean": "^4.3",
"ifsnop/mysqldump-php": "2.*",
"ezyang/htmlpurifier": "^4.8",
"codeguy/upload": "^1.3"
"codeguy/upload": "^1.3",
"php-imap/php-imap": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7"

0
server/config.php Normal file → Executable file
View File

View File

@ -14,7 +14,7 @@
*
* @apiParam {String} allowedLanguages The list of languages allowed.
* @apiParam {String} supportedLanguages The list of languages supported.
* @apiParam {Setting} setting A setting can be any of the following: language, recaptcha-public, recaptcha-private, no-reply-email, smtp-host, smtp-port, smtp-user, smtp-pass, time-zone, maintenance-mode, layout, allow-attachments, max-size, title, url.
* @apiParam {Setting} setting A setting can be any of the following: language, recaptcha-public, recaptcha-private, server-email, smtp-host, smtp-port, smtp-user, smtp-pass, time-zone, maintenance-mode, layout, allow-attachments, max-size, title, url.
*
* @apiUse NO_PERMISSION
*
@ -38,7 +38,10 @@ class EditSettingsController extends Controller {
'language',
'recaptcha-public',
'recaptcha-private',
'no-reply-email',
'server-email',
'imap-host',
'imap-user',
'imap-pass',
'smtp-host',
'smtp-port',
'smtp-user',

View File

@ -0,0 +1,116 @@
<?php
class EmailPolling extends Controller {
const PATH = '/email-polling';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() {
$commentController = new CommentController();
$createController = new CreateController();
$defaultLanguage = Setting::getSetting('language')->getValue();
$defaultDepartmentId = Department::getAll()->first()->id;
if(Controller::isUserSystemEnabled())
throw new RequestException(ERRORS::USER_SYSTEM);
$errors = [];
$emails = $this->getLastEmails();
$session = Session::getInstance();
$oldSession = [
'userId' => $session->getUserId(),
'staff' => $session->getToken(),
'token' => $session->isStaffLogged(),
];
foreach($emails as $email) {
Controller::setDataRequester(function ($key) use ($email, $defaultDepartmentId, $defaultLanguage) {
switch ($key) {
case 'ticketNumber':
return $email->getTicketNumber();
case 'title':
return $email->getSubject();
case 'content':
return $email->getContent();
case 'departmentId':
return $defaultDepartmentId;
case 'language':
return $defaultLanguage;
case 'email':
return $email->getSender();
case 'name':
return $email->getSenderName();
}
return null;
});
try {
if($email->isReply()) {
if($email->getTicket()->authorToArray()['email'] === $email->getSender()) {
$session->clearSessionData();
$session->createTicketSession($email->getTicket()->ticketNumber);
$commentController->handler();
}
} else {
$createController->handler();
}
} catch(\Exception $e) {
$errors[] = [
'author' => $email->getSender(),
'ticketNumber' => $email->getTicketNumber(),
'error' => $e->__toString(),
];
}
}
$session->clearSessionData();
$session->setSessionData($oldSession);
$this->eraseAllEmails();
if(count($errors)) {
Response::respondError(ERRORS::EMAIL_POLLING, null, $errors);
} else {
Response::respondSuccess();
}
}
public function getLastEmails() {
$mailbox = new \PhpImap\Mailbox(
Setting::getSetting('imap-host')->getValue(),
Setting::getSetting('imap-user')->getValue(),
Setting::getSetting('imap-pass')->getValue(),
__DIR__
);
$mailsIds = $mailbox->searchMailbox('ALL');
$emails = [];
sort($mailsIds);
foreach($mailsIds as $mailId) {
$mail = $mailbox->getMail($mailId);
$mailHeader = $mailbox->getMailHeader($mailId);
$emails[] = new Email([
'fromAddress' => $mailHeader->fromAddress,
'fromName' => $mailHeader->fromName,
'subject' => $mailHeader->subject,
'content' => $mail->textPlain,
'file' => null,
]);
}
return $emails;
}
public function eraseAllEmails() {
}
}

0
server/controllers/system/get-mail-template-list.php Normal file → Executable file
View File

View File

@ -17,7 +17,7 @@ DataValidator::with('CustomValidations', true);
* @apiParam {String} language Indicates the default language of the system.
* @apiParam {String} user-system-enabled Indicates if the user system should be enabled.
* @apiParam {String} registration Indicates if the registration should be enabled.
* @apiParam {String} no-reply-email Email from where automated emails will be sent.
* @apiParam {String} server-email Email from where automated emails will be sent.
* @apiParam {String} smtp-host SMTP Server address.
* @apiParam {String} smtp-port SMTP Server port.
* @apiParam {String} smtp-user SMTP Authentication User.
@ -68,11 +68,14 @@ class InitSettingsController extends Controller {
'language' => Controller::request('language'),
'recaptcha-public' => '',
'recaptcha-private' => '',
'no-reply-email' => Controller::request('no-reply-email'),
'server-email' => Controller::request('email'),
'imap-host' => Controller::request('imap-host'),
'imap-user' => Controller::request('imap-user'),
'imap-pass' => Controller::request('imap-pass'),
'smtp-host' => Controller::request('smtp-host'),
'smtp-port' => Controller::request('smtp-port'),
'smtp-user' => Controller::request('smtp-user'),
'smtp-pass' => Controller::request('smtp-password'),
'smtp-pass' => Controller::request('smtp-pass'),
'time-zone' => 0,
'maintenance-mode' => 0,
'layout' => 'boxed',

View File

@ -0,0 +1,44 @@
<?php
use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/test-imap Test IMAP Connection
* @apiVersion 4.3.2
*
* @apiName Test IMAP Connection
*
* @apiGroup System
*
* @apiDescription Test if the given values connect correctly to a IMAP server.
*
* @apiPermission any
*
* @apiParam {String} imap-host Host of the IMAP server.
* @apiParam {String} imap-user User for the IMAP server.
* @apiParam {String} imap-pass Password for the IMAP server.
*
* @apiUse SMTP_CONNECTION
*
* @apiSuccess {Object} data Empty object
*
*/
class TestSMTPController extends Controller {
const PATH = '/test-smtp';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() {
if(imap_open(Controller::request('imap-host'), Controller::request('imap-user'), Controller::request('imap-pass'), OP_SECURE)) {
Response::respondSuccess();
} else {
throw new RequestException(ERRORS::IMAP_CONNECTION);
}
}
}

4
server/controllers/system/test-smtp.php Normal file → Executable file
View File

@ -41,8 +41,8 @@ class TestSMTPController extends Controller {
Controller::request('smtp-host'),
Controller::request('smtp-port'),
Controller::request('smtp-user'),
Controller::request('smtp-password'),
Controller::request('no-reply-email')
Controller::request('smtp-pass'),
''
);
if($mailSender->isConnected()) {

View File

@ -139,6 +139,10 @@
* @apiDefine INVALID_BODY
* @apiError {String} INVALID_BODY The body is invalid.
*/
/**
* @apiDefine USER_SYSTEM_ENABLED
* @apiError {String} USER_SYSTEM_ENABLED The user system is enabled.
*/
/**
* @apiDefine USER_SYSTEM_DISABLED
* @apiError {String} USER_SYSTEM_DISABLED The user system is disabled.
@ -203,6 +207,10 @@
* @apiDefine DEPARTMENT_PRIVATE_TICKETS
* @apiError {String} DEPARTMENT_PRIVATE_TICKETS There are tickets for in department created by non-staff and it can't be private
*/
/**
* @apiDefine EMAIL_POLLING
* @apiError {String} EMAIL_POLLING Email polling
*/
class ERRORS {
const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS';
@ -241,6 +249,7 @@ class ERRORS {
const INVALID_TEMPLATE = 'INVALID_TEMPLATE';
const INVALID_SUBJECT = 'INVALID_SUBJECT';
const INVALID_BODY = 'INVALID_BODY';
const USER_SYSTEM_ENABLED = 'USER_SYSTEM_ENABLED';
const USER_SYSTEM_DISABLED = 'USER_SYSTEM_DISABLED';
const SYSTEM_USER_IS_ALREADY_DISABLED = 'SYSTEM_USER_IS_ALREADY_DISABLED';
const SYSTEM_USER_IS_ALREADY_ENABLED = 'SYSTEM_USER_IS_ALREADY_ENABLED';
@ -250,6 +259,7 @@ class ERRORS {
const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
const DATABASE_CREATION = 'DATABASE_CREATION';
const SMTP_CONNECTION = 'SMTP_CONNECTION';
const IMAP_CONNECTION = 'IMAP_CONNECTION';
const ALREADY_DISABLED = 'ALREADY_DISABLED';
const ALREADY_ENABLED = 'ALREADY_ENABLED';
const USER_DISABLED = 'USER_DISABLED';
@ -257,4 +267,5 @@ class ERRORS {
const INVALID_TEXT_2 = 'INVALID_TEXT_2';
const INVALID_TEXT_3 = 'INVALID_TEXT_3';
const DEPARTMENT_PRIVATE_TICKETS = 'DEPARTMENT_PRIVATE_TICKETS';
const EMAIL_POLLING = 'EMAIL_POLLING';
}

View File

@ -16,6 +16,11 @@ class DataStoreList implements IteratorAggregate {
return new ArrayIterator($this->list);
}
public function first() {
if(count($this->list)) return $this->list[0];
else return new NullDataStore();
}
public function add(DataStore $dataStore) {
$this->list[] = $dataStore;
}

71
server/libs/Email.php Executable file
View File

@ -0,0 +1,71 @@
<?php
class Email {
private $sender;
private $senderName;
private $ticket;
private $ticketNumber;
private $content;
private $attachment;
public function __construct($data) {
$this->sender = $this->parseSender($data);
$this->senderName = $this->parseSenderName($data);
$this->ticketNumber = $this->parseTicketNumber($data);
$this->ticket = $this->parseTicket($data);
$this->content = $this->parseContent($data);
$this->attachment = $this->parseAttachment($data);
$this->subject = $this->parseSubject($data);
}
public function isReply() { return !$this->ticket->isNull(); }
public function getSender() { return $this->sender; }
public function getTicket() { return $this->ticket; }
public function getSenderName() { return $this->senderName; }
public function getTicketNumber() { return $this->ticketNumber; }
public function getSubject() { return $this->subject; }
public function getContent() { return $this->content; }
public function getAttachment() { return $this->attachment; }
private function parseSender($data) {
return $data['fromAddress'];
}
private function parseSenderName($data) {
return $data['fromName'];
}
private function parseTicketNumber($data) {
return $this->parseTicketNumberFromString($data['subject']);
}
private function parseTicket() {
return Ticket::getByTicketNumber($this->ticketNumber);
}
private function parseSubject($data) {
return $data['subject'];
}
private function parseContent($data) {
return $data['content'];
}
private function parseAttachment($data) {
return $data['file'];
}
private function parseTicketNumberFromString($string) {
for($i=0; $i<strlen($string); $i++) {
if($string[$i] === '#') {
$match = substr($string, $i+1, 6);
if(strlen($match) === 6 && is_numeric($match)) {
return intval($match);
}
}
}
return null;
}
}

View File

@ -2,7 +2,7 @@
class MailSender {
use SingletonTrait;
private $mailOptions = [];
public $mailOptions = [];
private $mailerInstance;
private function __construct() {
@ -11,7 +11,7 @@ class MailSender {
Setting::getSetting('smtp-port')->getValue(),
Setting::getSetting('smtp-user')->getValue(),
Setting::getSetting('smtp-pass')->getValue(),
Setting::getSetting('no-reply-email')->getValue()
Setting::getSetting('server-email')->getValue()
);
}

View File

@ -3,11 +3,11 @@ class Response {
private static $response;
private static $responseException;
public static function respondError($errorMsg, $exception = null) {
public static function respondError($errorMsg, $exception = null, $data = null) {
self::$response = array(
'status' => 'fail',
'message' => $errorMsg,
'data' => null
'data' => $data
);
self::$responseException = $exception;

View File

@ -4,7 +4,7 @@ class Session {
use SingletonTrait;
private $sessionPrefix = '';
private function __construct() {
$this->initSession();
}
@ -18,17 +18,29 @@ class Session {
session_destroy();
}
public function clearSessionData() {
$this->store('userId', null);
$this->store('staff', null);
$this->store('token', null);
$this->store('ticketNumber', null);
}
public function setSessionData($data) {
foreach($data as $key => $value)
$this->store($key, $value);
}
public function createSession($userId, $staff = false) {
$this->store('userId', $userId);
$this->store('staff', $staff);
$this->store('token', Hashing::generateRandomToken());
}
public function createTicketSession($ticketNumber) {
$this->store('ticketNumber', $ticketNumber);
$this->store('token', Hashing::generateRandomToken());
}
public function getTicketNumber() {
return $this->getStoredData('ticketNumber');
}
@ -52,7 +64,7 @@ class Session {
public function checkAuthentication($data) {
$userId = $this->getStoredData('userId');
$token = $this->getStoredData('token');
return $userId && $token &&
$userId === $data['userId'] &&
$token === $data['token'];
@ -71,7 +83,7 @@ class Session {
return $storedValue;
}
public function isLoggedWithId($userId) {
return ($this->getStoredData('userId') === $userId);
}
@ -79,4 +91,4 @@ class Session {
public function setSessionPrefix($prefix) {
$this->sessionPrefix = $prefix;
}
}
}

View File

@ -2,3 +2,4 @@ source "https://rubygems.org"
gem 'mysql'
gem 'bacon'
gem 'mechanize'
gem 'mailfactory'

View File

@ -5,58 +5,59 @@ require 'uri'
require 'mysql'
require 'json'
require 'mechanize'
require 'mailfactory'
require './libs.rb'
require './scripts.rb'
# TESTS
require './system/init-settings.rb'
require './system/get-settings.rb'
require './system/edit-settings.rb'
require './user/signup.rb'
require './user/login.rb'
require './user/send-recover-password.rb'
require './user/recover-password.rb'
require './user/edit-password.rb'
require './user/edit-email.rb'
require './user/get.rb'
require './user/enable-disable.rb'
require './ticket/create.rb'
require './ticket/comment.rb'
require './ticket/get.rb'
require './ticket/custom-response.rb'
require './ticket/change-department.rb'
require './ticket/close.rb'
require './ticket/re-open.rb'
require './ticket/delete.rb'
require './staff/add.rb'
require './staff/get.rb'
require './staff/edit.rb'
require './staff/delete.rb'
require './staff/assign-ticket.rb'
require './staff/un-assign-ticket.rb'
require './staff/get-tickets.rb'
require './ticket/change-priority.rb'
require './staff/get-new-tickets.rb'
require './staff/get-all-tickets.rb'
require './ticket/events.rb'
require './article/topic.rb'
require './article/article.rb'
require './user/get-user.rb'
require './user/ban.rb'
require './user/get-users-test.rb'
require './user/delete.rb'
require './staff/get-all.rb'
require './system/add-department.rb'
require './system/edit-department.rb'
require './system/delete-department.rb'
require './staff/last-events.rb'
require './system/mail-templates.rb'
require './system/disable-registration.rb'
require './system/enable-registration.rb'
require './system/add-api-key.rb'
require './system/delete-api-key.rb'
require './system/get-api-keys.rb'
require './system/file-upload-download.rb'
require './system/csv-import.rb'
require './system/disable-user-system.rb'
require './system/get-stats.rb'
# require './system/init-settings.rb'
# require './system/get-settings.rb'
# require './system/edit-settings.rb'
# require './user/signup.rb'
# require './user/login.rb'
# require './user/send-recover-password.rb'
# require './user/recover-password.rb'
# require './user/edit-password.rb'
# require './user/edit-email.rb'
# require './user/get.rb'
# require './user/enable-disable.rb'
# require './ticket/create.rb'
# require './ticket/comment.rb'
# require './ticket/get.rb'
# require './ticket/custom-response.rb'
# require './ticket/change-department.rb'
# require './ticket/close.rb'
# require './ticket/re-open.rb'
# require './ticket/delete.rb'
# require './staff/add.rb'
# require './staff/get.rb'
# require './staff/edit.rb'
# require './staff/delete.rb'
# require './staff/assign-ticket.rb'
# require './staff/un-assign-ticket.rb'
# require './staff/get-tickets.rb'
# require './ticket/change-priority.rb'
# require './staff/get-new-tickets.rb'
# require './staff/get-all-tickets.rb'
# require './ticket/events.rb'
# require './article/topic.rb'
# require './article/article.rb'
# require './user/get-user.rb'
# require './user/ban.rb'
# require './user/get-users-test.rb'
# require './user/delete.rb'
# require './staff/get-all.rb'
# require './system/add-department.rb'
# require './system/edit-department.rb'
# require './system/delete-department.rb'
# require './staff/last-events.rb'
# require './system/mail-templates.rb'
# require './system/disable-registration.rb'
# require './system/enable-registration.rb'
# require './system/add-api-key.rb'
# require './system/delete-api-key.rb'
# require './system/get-api-keys.rb'
# require './system/file-upload-download.rb'
# require './system/csv-import.rb'
# require './system/disable-user-system.rb'
# require './system/get-stats.rb'

View File

@ -49,6 +49,61 @@ class Database
end
end
class MailServer
def initialize
@smtp_server = ENV['OPENSUPPORTS_SMTP_SERVER']
@smtp_port = ENV['OPENSUPPORTS_SMTP_PORT']
@imap_server = ENV['OPENSUPPORTS_IMAP_SERVER']
@imap_port = ENV['OPENSUPPORTS_IMAP_PORT']
@admin_user = ENV['OPENSUPPORTS_EMAIL_ADMIN_USERNAME']
@admin_password = ENV['OPENSUPPORTS_EMAIL_ADMIN_PASSWORD']
@client_user = ENV['OPENSUPPORTS_EMAIL_CLIENT_USERNAME']
@client_password = ENV['OPENSUPPORTS_EMAIL_CLIENT_PASSWORD']
@admin_imap = Net::IMAP.new(@imap_server, @imap_port, true)
@client_imap = Net::IMAP.new(@imap_server, @imap_port, true)
@admin_imap.login(@admin_user, @admin_password)
@client_imap.login(@client_user, @client_password)
@client_smtp = Net::SMTP.new(@smtp_server, @smtp_port).start(user=@client_user, secret=@client_password)
end
def clear_mails
self.clear_admin_mails
self.clear_client_mails
end
def clear_admin_mails
@admin_imap.delete('INBOX')
end
def clear_client_mails
@client_imap.delete('INBOX')
end
def send_mail(subject, text, file = nil)
message = MailFactory.new
message.to = @admin_user
message.from = @client_user
message.subject = subject
message.html = text
unless file.nil?
message.attach(file)
end
@client_smtp.send_message(message.to_s, @client_user, @admin_user)
Net::SMTP.start(@smtp_server, @smtp_port, @smtp_server, @client_user, @client_password) { |smtp|
smtp.send_message(message.to_s, @client_user, @admin_user)
}
end
end
$mail_server = MailServer.new
$database = Database.new
$staff = {

6
tests/system/edit-settings.rb Normal file → Executable file
View File

@ -3,7 +3,7 @@ describe'system/edit-settings' do
Scripts.login($staff[:email], $staff[:password], true)
it 'should edit settings' do
result= request('/system/edit-settings', {
result = request('/system/edit-settings', {
"csrf_userid" => $csrf_userid,
"csrf_token" => $csrf_token,
"maintenance-mode" => 0,
@ -12,7 +12,7 @@ describe'system/edit-settings' do
"allow-attachments" => 1,
"max-size" => 2,
"language" => 'en',
"no-reply-email" => 'testemail@hotmail.com'
"server-email" => 'testemail@hotmail.com'
})
(result['status']).should.equal('success')
@ -32,7 +32,7 @@ describe'system/edit-settings' do
row = $database.getRow('setting', 'language', 'name')
(row['value']).should.equal('en')
row = $database.getRow('setting', 'no-reply-email', 'name')
row = $database.getRow('setting', 'server-email', 'name')
(row['value']).should.equal('testemail@hotmail.com')
request('/user/logout')

9
tests/system/init-settings.rb Normal file → Executable file
View File

@ -17,11 +17,16 @@ describe '/system/init-settings' do
'user-system-enabled' => true,
'registration' => true,
'title' => 'Support Center',
'imap-host' => '{imap.dreamhost.com:993/imap/ssl}INBOX',
'imap-user' => "support@opensupports.com",
'imap-pass' => "gotaxc22",
'imap-user' => 'support@opensupports.com',
'imap-pass' => '',
'smtp-host' => 'localhost',
'smtp-port' => 7070,
'smtp-user' => 'noreply@opensupports.com',
'smtp-user' => 'support@opensupports.com',
'smtp-password' => '',
'no-reply-email' => 'noreply@opensupports.com',
'server-email' => 'support@opensupports.com',
'language' => 'en'
})