Merge branch 'hotfix/5.5.7'

This commit is contained in:
yuri
2019-01-31 11:00:26 +02:00
25 changed files with 357 additions and 151 deletions
+1 -1
View File
@@ -310,7 +310,7 @@ class CronManager
} else {
$this->runService($job);
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
$isSuccess = false;
if ($e->getCode() === -1) {
$job->set('attempts', 0);
+5 -1
View File
@@ -45,6 +45,10 @@ class JobTask extends \Spatie\Async\Task
public function run()
{
$app = new \Espo\Core\Application();
$app->runJob($this->jobId);
try {
$app->runJob($this->jobId);
} catch (\Throwable $e) {
$GLOBALS['log']->error("JobTask: Failed job run. Job id: ".$this->jobId.". Error details: ".$e->getMessage());
}
}
}
@@ -183,6 +183,7 @@ return [
'adminNotificationsCronIsNotConfigured',
'adminNotificationsNewExtensionVersion',
'leadCaptureAllowOrigin',
'cronDisabled',
],
'superAdminItems' => [
'jobMaxPortion',
@@ -196,6 +197,9 @@ return [
'daemonProcessTimeout',
'daemonMaxProcessNumber',
'adminPanelIframeUrl',
'cronDisabled',
'maintenanceMode',
'siteUrl',
],
'userItems' => [
'outboundEmailFromAddress',
+34 -8
View File
@@ -51,25 +51,51 @@ class Email extends \Espo\Core\ORM\Entity
return $this->has('fromString');
}
protected function _getFromName()
{
if (!$this->has('fromString')) return null;
return \Espo\Services\Email::parseFromName($this->get('fromString'));
}
protected function _hasFromAddress()
{
return $this->has('fromString');
}
protected function _hasReplyToName()
{
return $this->has('replyToString');
}
protected function _hasReplyToAddress()
{
return $this->has('replyToString');
}
protected function _getFromName()
{
if (!$this->has('fromString')) return null;
return \Espo\Services\Email::parseFromName($this->get('fromString'));
}
protected function _getFromAddress()
{
if (!$this->has('fromString')) return null;
return \Espo\Services\Email::parseFromAddress($this->get('fromString'));
}
protected function _getReplyToName()
{
if (!$this->has('replyToString')) return null;
$string = $this->get('replyToString');
if (!$string) return null;
$string = trim(explode(';', $string)[0]);
return \Espo\Services\Email::parseFromName($string);
}
protected function _getReplyToAddress()
{
if (!$this->has('replyToString')) return null;
$string = $this->get('replyToString');
if (!$string) return null;
$string = trim(explode(';', $string)[0]);
return \Espo\Services\Email::parseFromAddress($string);
}
protected function _setIsRead($value)
{
$this->setValue('isRead', $value !== false);
+1 -1
View File
@@ -66,7 +66,7 @@ class Formula extends \Espo\Core\Hooks\Base
}
}
$customScript = $this->getMetadata()->get(['formula', $entity->getEntityType(), 'beforeSaveCustomScript'], []);
$customScript = $this->getMetadata()->get(['formula', $entity->getEntityType(), 'beforeSaveCustomScript']);
if ($customScript) {
try {
$this->getFormulaManager()->run($customScript, $entity, $variables);
@@ -49,6 +49,8 @@
"fromName": "From Name",
"fromString": "From String",
"fromAddress": "From Address",
"replyToName": "Reply-To Name",
"replyToAddress": "Reply-To Address",
"isSystem": "Is System"
},
"links": {
@@ -114,7 +114,9 @@
"jobPoolConcurrencyNumber": "Jobs Pool Concurrency Number",
"daemonInterval": "Daemon Interval",
"daemonMaxProcessNumber": "Daemon Max Process Number",
"daemonProcessTimeout": "Daemon Process Timeout"
"daemonProcessTimeout": "Daemon Process Timeout",
"cronDisabled": "Disable Cron",
"maintenanceMode": "Maintenance Mode"
},
"options": {
"weekStart": {
@@ -178,11 +180,15 @@
"jobMaxPortion": "Max number of jobs processed per one execution.",
"daemonInterval": "Interval between process cron runs in seconds.",
"daemonMaxProcessNumber": "Max number of cron processes run simultaneously.",
"daemonProcessTimeout": "Max execution time (in seconds) allocated for a single cron process."
"daemonProcessTimeout": "Max execution time (in seconds) allocated for a single cron process.",
"cronDisabled": "Cron will not run.",
"maintenanceMode": "Only administrators will have access to the system."
},
"labels": {
"System": "System",
"Locale": "Locale",
"Search": "Search",
"Misc": "Misc",
"SMTP": "SMTP",
"Configuration": "Configuration",
"In-app Notifications": "In-app Notifications",
@@ -3,11 +3,22 @@
"label": "System",
"rows": [
[{"name": "useCache"}, {"name": "siteUrl"}],
[{"name": "exportDisabled"}, {"name": "globalSearchEntityList"}],
[{"name": "followCreatedEntities"}, {"name": "b2cMode"}],
[{"name": "aclStrictMode"}, {"name": "aclAllowDeleteCreated"}],
[{"name": "textFilterUseContainsForVarchar"}, {"name": "emailAddressIsOptedOutByDefault"}],
[{"name": "cleanupDeletedRecords"}, false]
[{"name": "b2cMode"}, {"name": "aclStrictMode"}],
[{"name": "maintenanceMode"}, {"name": "cronDisabled"}]
]
},
{
"label": "Search",
"rows": [
[{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}]
]
},
{
"label": "Misc",
"rows": [
[{"name": "followCreatedEntities"}, {"name": "emailAddressIsOptedOutByDefault"}],
[{"name": "aclAllowDeleteCreated"}, {"name": "cleanupDeletedRecords"}],
[{"name": "exportDisabled"}, false]
]
},
{
@@ -33,6 +33,20 @@
"replyToString": {
"type": "varchar"
},
"replyToName": {
"type": "varchar",
"readOnly": true,
"notStorable": true,
"textFilterDisabled": true,
"layoutFiltersDisabled": true
},
"replyToAddress": {
"type": "varchar",
"readOnly": true,
"notStorable": true,
"textFilterDisabled": true,
"layoutFiltersDisabled": true
},
"addressNameMap": {
"type": "jsonObject",
"disabled": true,
@@ -536,6 +536,14 @@
"daemonProcessTimeout": {
"type": "int",
"tooltip": true
},
"cronDisabled": {
"type": "bool",
"tooltip": true
},
"maintenanceMode": {
"type": "bool",
"tooltip": true
}
}
}
@@ -120,12 +120,11 @@ class EmailNotification extends \Espo\Core\Services\Base
$assignerUser = $this->getEntityManager()->getEntity('User', $assignerUserId);
$entity = $this->getEntityManager()->getEntity($entityType, $entityId);
$this->loadParentNameFields($entity);
if (!$entity) return true;
if (!$assignerUser) return true;
$this->loadParentNameFields($entity);
if (!$entity->hasLinkMultipleField('assignedUsers')) {
if ($entity->get('assignedUserId') !== $userId) return true;
}
+103 -73
View File
@@ -202,13 +202,13 @@ var Bull = Bull || {};
_wait: false,
expectedViews: null,
_waitViewList: null,
optionsToPass: null,
_nestedViewsFromLayoutLoaded: false,
_readyConditions: null,
_readyConditionList: null,
_isRendered: false,
@@ -243,12 +243,14 @@ var Bull = Bull || {};
this.nestedViews = {};
this._nestedViewDefs = {};
if (this.expectedViews == null) {
this.expectedViews = [];
if (this._waitViewList == null) {
this._waitViewList = [];
}
if (this._readyConditions == null) {
this._readyConditions = [];
this._waitPromiseCount = 0;
if (this._readyConditionList == null) {
this._readyConditionList = [];
}
this.optionsToPass = this.options.optionsToPass || this.optionsToPass || [];
@@ -279,6 +281,8 @@ var Bull = Bull || {};
this._layout = this.options._layout || this._layout;
this.layoutData = this.options.layoutData || this.layoutData;
this._template = this.templateContent || this._template;
if (this._template != null && this._templator.compilable) {
this._templateCompiled = this._templator.compileTemplate(this._template);
}
@@ -398,38 +402,46 @@ var Bull = Bull || {};
this._isRendered = false;
this._isFullyRendered = false;
this._getHtml(function (html) {
if (this._isRenderCanceled) {
this._isRenderCanceled = false;
this._isBeingRendered = false;
return;
}
if (this.$el.size()) {
this.$el.html(html);
} else {
if (this.options.el) {
this.setElement(this.options.el);
return new Promise(function (resolve, reject) {
this._getHtml(function (html) {
if (this._isRenderCanceled) {
this._isRenderCanceled = false;
this._isBeingRendered = false;
reject();
return;
}
this.$el.html(html);
}
this._afterRender();
if (typeof callback === 'function') {
callback();
}
if (this.$el.size()) {
this.$el.html(html);
} else {
if (this.options.el) {
this.setElement(this.options.el);
}
this.$el.html(html);
}
this._afterRender();
if (typeof callback === 'function') {
callback();
}
resolve(this);
}.bind(this));
}.bind(this));
},
/**
* Re-render view.
*/
reRender: function (force) {
if (this.isRendered()) {
this.render();
return this.render();
} else if (this.isBeingRendered()) {
this.once('after:render', function () {
this.render();
}, this);
return new Promise(function (resolve, reject) {
this.once('after:render', function () {
this.render().then(resolve).catch(reject);
}, this);
}.bind(this));
} else {
if (force) {
this.render();
return this.render();
}
}
},
@@ -455,29 +467,23 @@ var Bull = Bull || {};
afterRender: function () {},
_tryReady: function () {
if (this.isReady) {
return;
if (this.isReady) return;
if (this._wait) return;
if (!this._nestedViewsFromLayoutLoaded) return;
for (var i = 0; i < this._waitViewList.length; i++) {
if (!this.hasView(this._waitViewList[i])) return;
}
if (this._wait) {
return;
}
if (!this._nestedViewsFromLayoutLoaded) {
return;
}
for (var i in this.expectedViews) {
if (!this.hasView(this.expectedViews[i])) {
return;
}
}
for (var i in this._readyConditions) {
if (typeof this._readyConditions[i] === 'function') {
if (!this._readyConditions[i]()) {
return;
}
if (this._waitPromiseCount) return;
for (var i = 0; i < this._readyConditionList.length; i++) {
if (typeof this._readyConditionList[i] === 'function') {
if (!this._readyConditionList[i]()) return;
} else {
if (!this._readyConditions) {
return;
}
if (!this._readyConditionList) return;
}
}
@@ -669,6 +675,7 @@ var Bull = Bull || {};
if (this.model || null) {
data.model = this.model;
}
data.viewObject = this;
this.handleDataBeforeRender(data);
this._getTemplate(function (template) {
var html = this._renderer.render(template, data);
@@ -802,27 +809,30 @@ var Bull = Bull || {};
* @param {Bool} wait True be default. Set false if no need parent view wait for nested view loaded.
*/
createView: function (key, viewName, options, callback, wait) {
wait = (typeof wait === 'undefined') ? true : wait;
var context = this;
if (wait) {
this.waitForView(key);
}
options = options || {};
if (!options.el) {
options.el = this.getSelector() + ' [data-view="'+key+'"]';
}
this._factory.create(viewName, options, function (view) {
var isSet = false;
if (this._isRendered || options.setViewBeforeCallback) {
this.setView(key, view);
isSet = true;
return new Promise(function (resolve) {
wait = (typeof wait === 'undefined') ? true : wait;
var context = this;
if (wait) {
this.waitForView(key);
}
if (typeof callback === 'function') {
callback.call(context, view);
}
if (!this._isRendered && !options.setViewBeforeCallback && !isSet) {
this.setView(key, view);
options = options || {};
if (!options.el) {
options.el = this.getSelector() + ' [data-view="'+key+'"]';
}
this._factory.create(viewName, options, function (view) {
var isSet = false;
if (this._isRendered || options.setViewBeforeCallback) {
this.setView(key, view);
isSet = true;
}
if (typeof callback === 'function') {
callback.call(context, view);
}
resolve(view);
if (!this._isRendered && !options.setViewBeforeCallback && !isSet) {
this.setView(key, view);
}
}.bind(this));
}.bind(this));
},
@@ -897,7 +907,7 @@ var Bull = Bull || {};
* @param {Function} or {Bool}
*/
addReadyCondition: function (condition) {
this._readyConditions.push(condition);
this._readyConditionList.push(condition);
},
/**
@@ -905,14 +915,34 @@ var Bull = Bull || {};
* @param {String} key
*/
waitForView: function (key) {
this.expectedViews.push(key);
this._waitViewList.push(key);
},
/**
* Add wait condition is true is passed. Remove wait condition if false.
* @param {Bool}
* Make view wait for promise if Promise is passed as a parameter.
* Add wait condition if true is passed. Remove wait condition if false.
* @param wait Promise | Function | {Bool}
*/
wait: function (wait) {
if (typeof wait === 'object' && (wait instanceof Promise || typeof wait.then === 'function')) {
this._waitPromiseCount++;
wait.then(function () {
this._waitPromiseCount--;
this._tryReady();
}.bind(this));
return;
}
if (typeof wait == 'function') {
this._waitPromiseCount++;
var promise = new Promise(function (resolve) {
resolve(wait.call(this));
}.bind(this))
promise.then(function () {
this._waitPromiseCount--;
this._tryReady();
}.bind(this));
return promise;
}
if (wait) {
this._wait = true;
} else {
@@ -45,7 +45,7 @@ Espo.define('crm:views/call/fields/contacts', 'crm:views/meeting/fields/contacts
if (key in phoneNumbersMap) {
number = phoneNumbersMap[key];
var innerHtml = $(html).html();
innerHtml += '<span class="text-muted small"> &#187; </span> ' + '<a href="tel:'+number+'" class="small">' + number + '</a>';
innerHtml += '<span class="text-muted small"> &#187; </span> ' + '<a href="tel:'+number+'" class="small" data-phone-number="'+number+'" data-action="dial">' + number + '</a>';
html = '<div>' + innerHtml + '</div>';
}
@@ -45,7 +45,7 @@ Espo.define('crm:views/call/fields/leads', 'crm:views/meeting/fields/attendees',
if (key in phoneNumbersMap) {
number = phoneNumbersMap[key];
var innerHtml = $(html).html();
innerHtml += '<span class="text-muted small"> &#187; </span> ' + '<a href="tel:'+number+'" class="small">' + number + '</a>';
innerHtml += '<span class="text-muted small"> &#187; </span> ' + '<a href="tel:'+number+'" class="small" data-phone-number="'+number+'" data-action="dial">' + number + '</a>';
html = '<div>' + innerHtml + '</div>';
}
@@ -7,3 +7,4 @@
{{else}}
<div class="plain complex-text hidden">{{complexText value}}</div>
{{/unless}}
{{#unless isNotEmpty}}{{#if valueIsSet}}{{translate 'None'}}{{/if}}{{/unless}}
+20 -18
View File
@@ -40,24 +40,26 @@
modelFactory: null,
create: function (name, callback, context) {
context = context || this;
this.modelFactory.getSeed(name, function (seed) {
var orderBy = this.modelFactory.metadata.get(['entityDefs', name, 'collection', 'orderBy']);
var order = this.modelFactory.metadata.get(['entityDefs', name, 'collection', 'order']);
var className = this.modelFactory.metadata.get(['clientDefs', name, 'collection']) || 'collection';
Espo.loader.require(className, function (collectionClass) {
var collection = new collectionClass(null, {
name: name,
orderBy: orderBy,
order: order
});
collection.model = seed;
collection._user = this.modelFactory.user;
collection.entityType = name;
callback.call(context, collection);
return new Promise(function (resolve) {
context = context || this;
this.modelFactory.getSeed(name, function (seed) {
var orderBy = this.modelFactory.metadata.get(['entityDefs', name, 'collection', 'orderBy']);
var order = this.modelFactory.metadata.get(['entityDefs', name, 'collection', 'order']);
var className = this.modelFactory.metadata.get(['clientDefs', name, 'collection']) || 'collection';
Espo.loader.require(className, function (collectionClass) {
var collection = new collectionClass(null, {
name: name,
orderBy: orderBy,
order: order
});
collection.model = seed;
collection._user = this.modelFactory.user;
collection.entityType = name;
if (callback) {
callback.call(context, collection);
}
resolve(collection);
}.bind(this));
}.bind(this));
}.bind(this));
}
+9 -4
View File
@@ -49,10 +49,15 @@ Espo.define('model-factory', [], function () {
user: null,
create: function (name, callback, context) {
context = context || this;
this.getSeed(name, function (seed) {
var model = new seed();
callback.call(context, model);
return new Promise(function (resolve) {
context = context || this;
this.getSeed(name, function (seed) {
var model = new seed();
if (callback) {
callback.call(context, model);
}
resolve(model);
}.bind(this));
}.bind(this));
},
+8 -4
View File
@@ -32,11 +32,15 @@ Espo.define('views/admin/settings', 'views/settings/record/edit', function (Dep)
layoutName: 'settings',
afterRender: function () {
Dep.prototype.afterRender.call(this);
setup: function () {
Dep.prototype.setup.call(this);
},
if (this.getHelper().getAppParam('isRestrictedMode') && !this.getUser().isSuperAdmin()) {
this.hideField('cronDisabled');
this.hideField('maintenanceMode');
this.setFieldReadOnly('siteUrl');
}
}
});
});
+5 -1
View File
@@ -52,7 +52,11 @@ Espo.define('views/email/record/compose', ['views/record/edit', 'views/email/rec
}
if (!this.options.signatureDisabled && this.hasSignature()) {
var body = this.prependSignature(this.model.get('body') || '', this.model.get('isHtml'));
var addSignatureMethod = 'prependSignature';
if (this.options.appendSignature) {
addSignatureMethod = 'appendSignature';
}
var body = this[addSignatureMethod](this.model.get('body') || '', this.model.get('isHtml'));
this.model.set('body', body);
}
+38 -24
View File
@@ -314,30 +314,8 @@ Espo.define('views/fields/base', 'view', function (Dep) {
}, this);
if ((this.mode == 'detail' || this.mode == 'edit') && this.tooltip) {
var $a;
this.once('after:render', function () {
$a = $('<a href="javascript:" class="text-muted field-info"><span class="fas fa-info-circle"></span></a>');
var $label = this.getLabelElement();
$label.append(' ');
this.getLabelElement().append($a);
$a.popover({
placement: 'bottom',
container: 'body',
html: true,
content: (this.options.tooltipText || this.translate(this.name, 'tooltips', this.model.name)).replace(/\n/g, "<br />"),
trigger: 'click',
}).on('shown.bs.popover', function () {
$('body').one('click', function () {
$a.popover('hide');
});
});
}, this);
this.on('remove', function () {
if ($a) {
$a.popover('destroy')
}
}, this);
if ((this.isDetailMode() || this.isEditMode()) && this.tooltip) {
this.initTooltip();
}
if (this.mode == 'detail') {
@@ -375,6 +353,42 @@ Espo.define('views/fields/base', 'view', function (Dep) {
}
},
initTooltip: function () {
var $a;
this.once('after:render', function () {
$a = $('<a href="javascript:" class="text-muted field-info"><span class="fas fa-info-circle"></span></a>');
var $label = this.getLabelElement();
$label.append(' ');
this.getLabelElement().append($a);
$a.popover({
placement: 'bottom',
container: 'body',
html: true,
content: (this.options.tooltipText || this.translate(this.name, 'tooltips', this.model.name)).replace(/\n/g, "<br />"),
}).on('shown.bs.popover', function () {
$('body').off('click.popover-' + this.id);
$('body').on('click.popover-' + this.id , function (e) {
if (e.target.classList.contains('popover-content')) return;
if ($.contains($a.get(0), e.target)) return;
$('body').off('click.popover-' + this.id);
$a.popover('hide');
}.bind(this));
}.bind(this));
$a.on('click', function () {
$(this).popover('toggle');
});
}, this);
this.on('remove', function () {
if ($a) {
$a.popover('destroy')
}
$('body').off('click.popover-' + this.id);
}, this);
},
showRequiredSign: function () {
var $label = this.getLabelElement();
var $sign = $label.find('span.required-sign');
+5 -1
View File
@@ -434,7 +434,11 @@ Espo.define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], fun
fetch: function () {
var data = {};
if (!this.model.has('isHtml') || this.model.get('isHtml')) {
data[this.name] = this.$summernote.summernote('code');
var code = this.$summernote.summernote('code');
if (code == '<p><br></p>') {
code = '';
}
data[this.name] = code;
} else {
data[this.name] = this.$element.val();
}
+7 -3
View File
@@ -89,12 +89,16 @@ Espo.define('views/import/step1', 'view', function (Dep) {
{key: "YYYY.MM.DD", value: '2017.12.27'}
],
timeFormatDataList: [
{key: "HH:mm", value: '23:00'},
{key: "HH:mm:ss", value: '23:00:00'},
{key: "HH:mm", value: '23:00'},
{key: "hh:mm a", value: '11:00 pm'},
{key: "hh:mma", value: '11:00pm'},
{key: "hh:mm A", value: '11:00 PM'},
{key: "hh:mmA", value: '11:00PM'}
{key: "hh:mmA", value: '11:00PM'},
{key: "hh:mm:ss a", value: '11:00:00 pm'},
{key: "hh:mm:ssa", value: '11:00:00pm'},
{key: "hh:mm:ss A", value: '11:00:00 PM'},
{key: "hh:mm:ssA", value: '11:00:00PM'},
],
timezoneList: this.getMetadata().get(['entityDefs', 'Settings', 'fields', 'timeZone', 'options'])
};
@@ -107,7 +111,7 @@ Espo.define('views/import/step1', 'view', function (Dep) {
delimiter: ',',
textQualifier: '"',
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm',
timeFormat: 'HH:mm:ss',
timezone: 'UTC',
decimalMark: '.',
personNameFormat: 'f l',
+1
View File
@@ -88,6 +88,7 @@ Espo.define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
selectTemplateDisabled: this.options.selectTemplateDisabled,
removeAttachmentsOnSelectTemplate: this.options.removeAttachmentsOnSelectTemplate,
signatureDisabled: this.options.signatureDisabled,
appendSignature: this.options.appendSignature,
exit: function () {}
};
this.createView('edit', viewName, options, callback);
+8
View File
@@ -1565,6 +1565,14 @@ table.less-padding td.cell[data-name="buttons"] > .btn-group {
top: 0;
}
.note-editor {
.note-editable {
min-height: 39px;
}
}
.panel-body.no-padding {
padding-left: 0 !important;
padding-top: 0 !important;
@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\integration\Espo\Email;
class EmailEntityTest extends \tests\integration\Core\BaseTestCase
{
public function testFromFields()
{
$entityManager = $this->getContainer()->get('entityManager');
$email = $entityManager->getEntity('Email');
$email->set('fromString', 'Test Hello <test@test.com>');
$this->assertEquals('test@test.com', $email->get('fromAddress'));
$this->assertEquals('Test Hello', $email->get('fromName'));
}
public function testReplyToFields()
{
$entityManager = $this->getContainer()->get('entityManager');
$email = $entityManager->getEntity('Email');
$email->set('replyToString', 'Test Hello <test@test.com>; Man Test <man@test.com>');
$this->assertEquals('test@test.com', $email->get('replyToAddress'));
$this->assertEquals('Test Hello', $email->get('replyToName'));
}
}