diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1df9ab6..bf65411 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
- php: [ '7.3', '7.4', '8.0', '8.1' ]
+ php: [ '7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- name: Checkout
diff --git a/.gitignore b/.gitignore
index 2411a82..1926d06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ composer.lock
# phpunit itself is not needed
phpunit.phar
+.phpunit.result.cache
+tests/log
diff --git a/composer.json b/composer.json
index 2906a86..d052f3b 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,8 @@
"ext-soap": "*",
"yiisoft/yii2": "^2.0.20",
"php-ews/php-ews": "^1.0.0",
- "simialbi/yii2-simialbi-base": ">=0.13.1 <1.0 | ^1.0.0"
+ "simialbi/yii2-simialbi-base": ">=0.13.1 <1.0 | ^1.0.0",
+ "simshaun/recurr": "^5.0.0"
},
"require-dev": {
"yiisoft/yii2-coding-standards": "~2.0",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 5311924..7ac2524 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,4 +11,19 @@
./tests
+
+
+
+
+
+
+ ./src/conditions
+ ./src/models
+ ./src/recurrence/transformers
+
+ /path/to/files
+ ./tests/TestCase.php
+
+
+
diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php
index b1f0f54..3dd03c0 100644
--- a/src/ActiveRecord.php
+++ b/src/ActiveRecord.php
@@ -57,7 +57,7 @@ public static function primaryKey(): array
/**
* {@inheritDoc}
- * @throws \yii\base\InvalidConfigException
+ * @throws InvalidConfigException
*/
public static function find(): ActiveQuery
{
@@ -67,7 +67,7 @@ public static function find(): ActiveQuery
/**
* {@inheritDoc}
* @return \simialbi\yii2\ews\Connection
- * @throws \yii\base\InvalidConfigException
+ * @throws InvalidConfigException
*/
public static function getDb(): Connection
{
@@ -77,7 +77,7 @@ public static function getDb(): Connection
/**
* {@inheritDoc}
* @return boolean
- * @throws \yii\base\NotSupportedException|Exception|InvalidConfigException
+ * @throws \yii\base\NotSupportedException|Exception|InvalidConfigException|\ReflectionException
*/
public static function updateAll($attributes, $condition = null): bool
{
@@ -207,7 +207,7 @@ public function insert($runValidation = true, $attributes = null, array $params
*
* @param string|null $value The date time string
* @return string|null The formatted date time string
- * @throws \yii\base\InvalidConfigException
+ * @throws InvalidConfigException
*/
public function typeCastDateTime(?string $value): ?string
{
diff --git a/src/Client.php b/src/Client.php
index 24921ae..4b2268f 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -9,6 +9,7 @@
use jamesiarmes\PhpEws\ClassMap;
use jamesiarmes\PhpNtlm\SoapClient;
use Yii;
+use yii\base\InvalidConfigException;
class Client extends \jamesiarmes\PhpEws\Client
{
@@ -17,6 +18,22 @@ class Client extends \jamesiarmes\PhpEws\Client
*/
protected string $location;
+ /**
+ * {@inheritDoc}
+ * @param string $timezone The default timezone to set (Exchange format). Defaults to `W. Europe standard time`.
+ * @throws InvalidConfigException
+ */
+ public function __construct($server = null, $username = null, $password = null, $version = self::VERSION_2013, ?string $timezone = null)
+ {
+ parent::__construct($server, $username, $password, $version);
+
+ if (null === $timezone) {
+ $this->autoSetTimezone();
+ } else {
+ $this->setTimezone($timezone);
+ }
+ }
+
/**
* Sets the location property
*
@@ -50,4 +67,60 @@ protected function initializeSoapClient(): SoapClient
return $this->soap;
}
+
+ /**
+ * Try to set exchange timezone from app configuration
+ *
+ * @return void
+ * @throws InvalidConfigException
+ */
+ private function autoSetTimezone(): void
+ {
+ $list = timezone_abbreviations_list();
+ $offset = 0;
+ $timezoneName = Yii::$app->formatter->asDate('now', 'VV');
+
+ foreach ($list as $items) {
+ foreach ($items as $item) {
+ if ($item['dst']) {
+ continue 2;
+ }
+ if ($item['timezone_id'] === $timezoneName) {
+ $offset = $item['offset'];
+ break 2;
+ }
+ }
+ }
+
+ $this->setTimezone(match ($offset) {
+ -39600 => 'UTC-11',
+ -36000 => 'Hawaiian Standard Time',
+ -28800 => 'Pacific Standard Time',
+ -25200 => 'Mountain Standard Time',
+ -21600 => 'Central America Standard Time',
+ -18000 => 'Eastern Standard Time',
+ -16200 => 'Venezuela Standard Time',
+ -14400 => 'SA Western Standard Time',
+ -10800 => 'Pacific SA Standard Time',
+ -7200 => 'UTC-02',
+ -3600 => 'Cape Verde Standard Time',
+ default => 'UTC',
+ 3600 => 'W. Europe Standard Time',
+ 7200 => 'FLE Standard Time',
+ 10800 => 'Arab Standard Time',
+ 12600 => 'Iran Standard Time',
+ 14400 => 'Caucasus Standard Time',
+ 19800 => 'India Standard Time',
+ 20700 => 'Nepal Standard Time',
+ 21600 => 'Bangladesh Standard Time',
+ 23400 => 'Myanmar Standard Time',
+ 25200 => 'SE Asia Standard Time',
+ 28800 => 'Singapore Standard Time',
+ 32400 => 'Tokyo Standard Time',
+ 36000 => 'AUS Eastern Standard Time',
+ 39600 => 'Central Pacific Standard Time',
+ 43200 => 'UTC+12',
+ 46800 => 'Samoa Standard Time'
+ });
+ }
}
diff --git a/src/Command.php b/src/Command.php
index cac81d8..c016e71 100644
--- a/src/Command.php
+++ b/src/Command.php
@@ -230,9 +230,13 @@ public function execute(): \jamesiarmes\PhpEws\Type\ItemIdType|bool
switch ($method) {
case 'CreateItem':
case 'UpdateItem':
- if (isset($message->Items->Message)) {
- $return = $message->Items->Message[0]->ItemId;
- } else {
+ foreach (['Message', 'CalendarItem', 'Task', 'Item'] as $itemType) {
+ if (isset($message->Items->{$itemType}[0])) {
+ $return = $message->Items->{$itemType}[0]->ItemId;
+ break;
+ }
+ }
+ if (!$return) {
foreach ($message->Items as $item) {
/** @var \jamesiarmes\PhpEws\Type\ItemType[] $item */
$return = $item[0]->ItemId;
@@ -299,35 +303,13 @@ protected function queryInternal(?string $method = null): mixed
$message = ArrayHelper::getValue($response, "ResponseMessages.{$method}ResponseMessage");
if (is_array($message)) {
- $message = array_shift($message);
- }
-
- if ($message->ResponseCode !== ResponseCodeType::NO_ERROR) {
- throw new Exception($message->MessageText, [
- 'responseCode' => $message->ResponseCode,
- 'responseClass' => $message->ResponseClass
- ]);
- }
-
- $result = match ($method) {
- 'FindItem' => ArrayHelper::getValue($message, 'RootFolder.Items'),
- 'FindFolder' => ArrayHelper::getValue($message, 'RootFolder.Folders'),
- 'GetItem' => ArrayHelper::getValue($message, 'Items'),
- default => [],
- };
-
- // TODO: Find better solution
- if (!is_array($result)) {
- $r = new \ReflectionClass($result);
- foreach ($r->getProperties() as $property) {
- if ($property->isPublic() && is_array($result->{$property->name}) && !empty($result->{$property->name})) {
- $result = $result->{$property->name};
- break;
- }
- }
- }
- if ($result instanceof \jamesiarmes\PhpEws\ArrayType\ArrayOfRealItemsType) {
$result = [];
+ while ($m = array_shift($message)) {
+ $tmp = $this->getResult($m, $method);
+ $result = array_merge($result, $tmp);
+ }
+ } else {
+ $result = $this->getResult($message, $method);
}
} catch (Exception $e) {
throw $e;
@@ -346,4 +328,49 @@ protected function queryInternal(?string $method = null): mixed
return $result;
}
+
+ /**
+ * @param mixed $message
+ * @param string|null $method
+ * @return array|mixed
+ * @throws Exception
+ * @throws \ReflectionException
+ * @throws \Exception
+ */
+ protected function getResult(mixed $message, ?string $method): mixed
+ {
+ if ($message->ResponseCode !== ResponseCodeType::NO_ERROR) {
+ if ($message->ResponseCode === 'ErrorItemNotFound') {
+ return [];
+ }
+ throw new Exception($message->MessageText, [
+ 'responseCode' => $message->ResponseCode,
+ 'responseClass' => $message->ResponseClass
+ ]);
+ }
+
+ $result = match ($method) {
+ 'FindItem' => ArrayHelper::getValue($message, 'RootFolder.Items'),
+ 'FindFolder' => ArrayHelper::getValue($message, 'RootFolder.Folders'),
+ 'GetItem' => ArrayHelper::getValue($message, 'Items'),
+ default => [],
+ };
+
+ // TODO: Find better solution
+ if (!is_array($result)) {
+ $r = new \ReflectionClass($result);
+ foreach ($r->getProperties() as $property) {
+ if ($property->isPublic() && is_array($result->{$property->name}) && !empty($result->{$property->name})) {
+ $result = $result->{$property->name};
+ break;
+ }
+ }
+ }
+
+ if ($result instanceof \jamesiarmes\PhpEws\ArrayType\ArrayOfRealItemsType) {
+ $result = [];
+ }
+
+ return $result;
+ }
}
diff --git a/src/Connection.php b/src/Connection.php
index 4758487..95585f6 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -103,6 +103,7 @@ class Connection extends Component
/**
* Get the [[Client]] instance.
* @return Client
+ * @throws InvalidConfigException
*/
public function getClient(): Client
{
diff --git a/src/Exception.php b/src/Exception.php
index 19d9af6..3fb1bc7 100644
--- a/src/Exception.php
+++ b/src/Exception.php
@@ -6,7 +6,6 @@
namespace simialbi\yii2\ews;
-
/**
* Exception represents an exception that is caused by some EWS-related operations.
*
diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php
index 1b209aa..ba137b4 100644
--- a/src/QueryBuilder.php
+++ b/src/QueryBuilder.php
@@ -13,7 +13,9 @@
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfFieldOrdersType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfItemChangeDescriptionsType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfItemChangesType;
+use jamesiarmes\PhpEws\Enumeration\AffectedTaskOccurrencesType;
use jamesiarmes\PhpEws\Enumeration\CalendarItemCreateOrDeleteOperationType;
+use jamesiarmes\PhpEws\Enumeration\CalendarItemUpdateOperationType;
use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
use jamesiarmes\PhpEws\Enumeration\DisposalType;
use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType;
@@ -47,11 +49,16 @@
use jamesiarmes\PhpEws\Type\SetItemFieldType;
use jamesiarmes\PhpEws\Type\TargetFolderIdType;
use jamesiarmes\PhpEws\Type\TaskType;
+use Recurr\Exception\InvalidRRule;
+use Recurr\Rule;
use simialbi\yii2\ews\models\CalendarEvent;
use simialbi\yii2\ews\models\Contact;
use simialbi\yii2\ews\models\Folder;
use simialbi\yii2\ews\models\Message;
+use simialbi\yii2\ews\models\Task;
+use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer;
use Yii;
+use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\db\ExpressionInterface;
use yii\helpers\ArrayHelper;
@@ -107,7 +114,22 @@ public function build($query, $params = []): array
$this->_config = [
'class' => FindItemType::class
];
- if (isset($query->where['id'], $query->where['changeKey'])) {
+ if (isset($query->where['id'])) {
+ if (!is_array($query->where['id'])) {
+ $query->where['id'] = [$query->where['id']];
+ }
+ if (isset($query->where['changeKey']) && !is_array($query->where['changeKey'])) {
+ $query->where['changeKey'] = [$query->where['changeKey']];
+ }
+ $ids = [];
+
+ for ($i = 0; $i < count($query->where['id']); $i++) {
+ $ids[] = Yii::createObject([
+ 'class' => ItemIdType::class,
+ 'Id' => $query->where['id'][$i],
+ 'ChangeKey' => $query->where['changeKey'][$i] ?? null
+ ]);
+ }
$this->_config = [
'class' => GetItemType::class,
'ItemShape' => Yii::createObject([
@@ -116,20 +138,13 @@ public function build($query, $params = []): array
]),
'ItemIds' => Yii::createObject([
'class' => NonEmptyArrayOfBaseItemIdsType::class,
- 'ItemId' => [
- Yii::createObject([
- 'class' => ItemIdType::class,
- 'Id' => $query->where['id'],
- 'ChangeKey' => $query->where['changeKey']
- ])
- ]
+ 'ItemId' => $ids
])
];
} else {
if (empty($params['folderId'])) {
$params['folderId'] = DistinguishedFolderIdNameType::INBOX;
if ($query instanceof ActiveQuery) {
- /** @var ActiveQuery $query */
$this->_modelClass = $query->modelClass;
switch ($this->_modelClass) {
@@ -142,6 +157,9 @@ public function build($query, $params = []): array
case Contact::class:
$params['folderId'] = DistinguishedFolderIdNameType::CONTACTS;
break;
+ case Task::class:
+ $params['folderId'] = DistinguishedFolderIdNameType::TODO_SEARCH;
+ break;
}
}
}
@@ -375,6 +393,9 @@ public function update($table, $columns, $condition, &$params): bool|BaseRequest
if ($table::modelName() === MessageType::class) {
$config['MessageDisposition'] = MessageDispositionType::SAVE_ONLY;
}
+ if ($table::modelName() === CalendarItemType::class) {
+ $config['SendMeetingInvitationsOrCancellations'] = CalendarItemUpdateOperationType::SEND_ONLY_TO_CHANGED;
+ }
return Yii::createObject($config);
}
@@ -382,7 +403,7 @@ public function update($table, $columns, $condition, &$params): bool|BaseRequest
/**
* {@inheritDoc}
*
- * @param string $table active record class name
+ * @param string|ActiveRecord $table active record class name
*
* @return BaseRequestType|object|false
* @throws \yii\base\InvalidConfigException
@@ -409,6 +430,13 @@ public function delete($table, $condition, &$params): bool|BaseRequestType
])
];
+ if ($table::modelName() === CalendarItemType::class) {
+ $config['SendMeetingCancellations'] = CalendarItemCreateOrDeleteOperationType::SEND_TO_ALL_AND_SAVE_COPY;
+ }
+ if ($table::modelName() === TaskType::class) {
+ $config['AffectedTaskOccurrences'] = AffectedTaskOccurrencesType::ALL;
+ }
+
return Yii::createObject($config);
}
@@ -525,10 +553,9 @@ public function buildCondition($condition, &$params): object|array
*/
public function buildExpression(ExpressionInterface $expression, &$params = []): object|array
{
+ /** @var object|ExpressionInterface $expression */
$expression = parent::buildExpression($expression, $params);
- /** @var object $expression */
-
return $expression;
}
@@ -561,9 +588,12 @@ public function getUriFromProperty(string $property): ?string
case 'subject':
return UnindexedFieldURIType::ITEM_SUBJECT;
case 'body':
+ case 'format':
return UnindexedFieldURIType::ITEM_BODY;
case 'type':
return UnindexedFieldURIType::CALENDAR_ITEM_TYPE;
+ case 'recurrence':
+ return UnindexedFieldURIType::CALENDAR_RECURRENCE;
case 'isRecurring':
return UnindexedFieldURIType::CALENDAR_IS_RECURRING;
case 'isAllDay':
@@ -578,6 +608,8 @@ public function getUriFromProperty(string $property): ?string
return UnindexedFieldURIType::ITEM_LAST_MODIFIED_TIME;
case 'createdAt':
return UnindexedFieldURIType::ITEM_DATE_TIME_CREATED;
+ case 'attachments':
+ return UnindexedFieldURIType::ITEM_ATTACHMENTS;
}
break;
case Message::class:
@@ -604,6 +636,8 @@ public function getUriFromProperty(string $property): ?string
return UnindexedFieldURIType::ITEM_DATE_TIME_CREATED;
case 'updatedAt':
return UnindexedFieldURIType::ITEM_LAST_MODIFIED_TIME;
+ case 'attachments':
+ return UnindexedFieldURIType::ITEM_ATTACHMENTS;
}
break;
case Folder::class:
@@ -627,7 +661,7 @@ public function getUriFromProperty(string $property): ?string
/**
* {@inheritDoc}
- * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException
+ * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException|InvalidRRule
*/
protected function prepareInsertValues($table, $columns, $params = []): array
{
@@ -694,36 +728,73 @@ protected function prepareUpdateSets($table, $columns, $params = []): array
try {
$value = $this->castDataType($mapping[$name]['dataType'], $value, false, $params);
- } catch (NotSupportedException $e) {
+ } catch (NotSupportedException|InvalidRRule) {
continue;
}
$val = [
'class' => $table::modelName()
];
- if ($mapping[$name]['foreignModel']) {
+ if (!empty($mapping[$name]['foreignModel'])) {
$tmp = explode('.', $mapping[$name]['foreignField']);
$field = array_shift($tmp);
- $val[$field] = Yii::createObject(ArrayHelper::merge(
+ $val[$field] = !is_object($value) ? Yii::createObject(ArrayHelper::merge(
['class' => $mapping[$name]['foreignModel']],
[$tmp[0] => $value]
- ));
+ )) : $value;
} else {
$val[$mapping[$name]['foreignField']] = $value;
}
- $changes[] = Yii::createObject([
+ $changes[$uri] = $this->merge($changes[$uri] ?? [], $val);
+ }
+
+ return array_map(function (string $uri, array $item) use ($property): SetItemFieldType {
+ /** @var SetItemFieldType $obj */
+ $obj = Yii::createObject([
'class' => SetItemFieldType::class,
'FieldURI' => Yii::createObject([
'class' => PathToUnindexedFieldType::class,
'FieldURI' => $uri
]),
- $property => Yii::createObject($val)
+ $property => Yii::createObject($item)
]);
+
+ return $obj;
+ }, array_keys($changes), array_values($changes));
+ }
+
+ /**
+ * Merge two objects or arrays recursively
+ * @param array|object $a
+ * @param array|object $b
+ * @return array|object
+ * @throws InvalidConfigException
+ */
+ protected function merge(array|object $a, array|object $b): array|object
+ {
+ foreach ($b as $k => $v) {
+ if (is_object($a)) {
+ if (is_array($v)) {
+ $a->$k = ArrayHelper::merge($a->$k, $b->$k);
+ } elseif (is_object($v)) {
+ $a->$k = $this->merge($a->$k, $v);
+ } elseif (!empty($b->$k)) {
+ $a->$k = $b->$k;
+ }
+ } else {
+ if (is_array($v)) {
+ $a[$k] = ArrayHelper::merge($a[$k], $v);
+ } elseif (is_object($v)) {
+ $a[$k] = $this->merge($a[$k] ?? Yii::createObject(['class' => $v::class]), $v);
+ } elseif (!empty($v)) {
+ $a[$k] = $v;
+ }
+ }
}
- return $changes;
+ return $a;
}
/**
@@ -732,13 +803,15 @@ protected function prepareUpdateSets($table, $columns, $params = []): array
* @param boolean $isInsert
* @param array $params
* @return array|bool|float|int|ActiveRecord|string
- * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException
+ * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException|InvalidRRule
*/
protected function castDataType(array $dataType, mixed $value, bool $isInsert = true, array $params = []): mixed
{
if (count($dataType) > 1) {
if (false !== in_array('\\DateTime', $dataType)) {
$dataType = 'DateTime';
+ } elseif (false !== in_array('\\Recurr\\Rule', $dataType)) {
+ $dataType = 'Recurrence';
} else {
$dataType = 'string';
}
@@ -776,6 +849,16 @@ protected function castDataType(array $dataType, mixed $value, bool $isInsert =
case 'DateTime':
$value = Yii::$app->formatter->asDatetime($value, 'yyyy-MM-dd\'T\'HH:mm:ssxxx');
break;
+ case 'Recurrence':
+ if (empty($value)) {
+ break;
+ }
+ $t = new ExchangeTransformer();
+ if (is_string($value)) {
+ $value = Rule::createFromString($value);
+ }
+ $value = $t->transformRecurrenceToEws($value);
+ break;
default:
if (!$isInsert) {
throw new NotSupportedException();
diff --git a/src/conditions/HashConditionBuilder.php b/src/conditions/HashConditionBuilder.php
index 885de56..1f6d197 100644
--- a/src/conditions/HashConditionBuilder.php
+++ b/src/conditions/HashConditionBuilder.php
@@ -25,10 +25,10 @@ class HashConditionBuilder extends \yii\db\conditions\HashConditionBuilder
/**
* {@inheritDoc}
- * @return array
+ * @return array|object
* @throws \yii\base\InvalidConfigException
*/
- public function build(ExpressionInterface $expression, array &$params = []): array
+ public function build(ExpressionInterface $expression, array &$params = []): array|object
{
/** @var \yii\db\conditions\HashCondition $expression */
$hash = $expression->getHash();
diff --git a/src/models/Attachment.php b/src/models/Attachment.php
index 6c374d9..f8e7e10 100644
--- a/src/models/Attachment.php
+++ b/src/models/Attachment.php
@@ -7,7 +7,7 @@
use jamesiarmes\PhpEws\Type\AttachmentType;
use jamesiarmes\PhpEws\Type\RequestAttachmentIdType;
use simialbi\yii2\ews\ActiveRecord;
-use yii\helpers\ArrayHelper;
+use yii\base\InvalidConfigException;
/**
* @property string $id => \jamesiarmes\PhpEws\Type\AttachmentIdType:AttachmentId.Id
@@ -16,19 +16,14 @@
* @property string $contentId => ContentId
* @property string $location => ContentLocation
* @property string $mime => ContentType
- * @property bool $isInline => IsInline
+ * @property boolean $isInline => IsInline
* @property string $name => Name
- * @property int $size => Size
- *
- * @property-read $content
+ * @property integer $size => Size
+ * @property string $content => \jamesiarmes\PhpEws\Type\FileAttachmentType:Content
+ * @property boolean $isContactPhoto => \jamesiarmes\PhpEws\Type\FileAttachmentType:IsContactPhoto
*/
class Attachment extends ActiveRecord
{
- /**
- * @var string The binary content
- */
- private string $_content;
-
/**
* {@inheritDoc}
*/
@@ -43,31 +38,24 @@ public static function modelName(): string
public function rules(): array
{
return [
- [['id', 'changeKey', 'contentId', 'location', 'mime', 'name'], 'string'],
- [['isInline'], 'boolean'],
- ['size', 'int'],
+ [['id', 'rootId', 'rootChangeKey', 'contentId', 'location', 'mime', 'name', 'content'], 'string'],
+ [['isInline', 'isContactPhoto'], 'boolean'],
+ ['size', 'integer'],
- ['isInline', 'default', 'value' => false]
+ [['isInline', 'isContactPhoto'], 'default', 'value' => false]
];
}
/**
* {@inheritDoc}
- */
- public function fields(): array
- {
- return ArrayHelper::merge(parent::fields(), ['content' => 'content']);
- }
-
- /**
- * Load binary content
- * @return string
- * @throws \yii\base\InvalidConfigException
+ * @throws InvalidConfigException
* @todo Find better solution
*/
- public function getContent(): string
+ public function afterFind(): void
{
- if (!isset($this->_content)) {
+ parent::afterFind();
+
+ if (empty($this->content)) {
$request = new GetAttachmentType();
$request->AttachmentIds = new NonEmptyArrayOfRequestAttachmentIdsType();
@@ -78,9 +66,11 @@ public function getContent(): string
$response = self::getDb()->getClient()->GetAttachment($request);
$message = $response->ResponseMessages->GetAttachmentResponseMessage[0];
- $this->_content = $message->Attachments->FileAttachment[0]->Content;
- }
+ $this->content = $message->Attachments->FileAttachment[0]->Content;
+ $this->isContactPhoto = $message->Attachments->FileAttachment[0]->IsContactPhoto;
- return $this->_content;
+ $this->setOldAttribute('content', $this->content);
+ $this->setOldAttribute('isContactPhoto', $this->isContactPhoto);
+ }
}
}
diff --git a/src/models/CalendarEvent.php b/src/models/CalendarEvent.php
index cd92f34..137a4dd 100644
--- a/src/models/CalendarEvent.php
+++ b/src/models/CalendarEvent.php
@@ -10,8 +10,10 @@
use jamesiarmes\PhpEws\Enumeration\CalendarItemTypeType;
use jamesiarmes\PhpEws\Enumeration\LegacyFreeBusyType;
use jamesiarmes\PhpEws\Type\CalendarItemType;
+use jamesiarmes\PhpEws\Type\RecurrenceType;
+use Recurr\Rule;
use simialbi\yii2\ews\ActiveRecord;
-use Yii;
+use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer;
use yii\behaviors\AttributeTypecastBehavior;
/**
@@ -29,13 +31,15 @@
* @property string $format => \jamesiarmes\PhpEws\Type\BodyType:Body.BodyType
* @property string $location => Location
* @property string $type => CalendarItemType
- * @property boolean $isRecurring => IsRecurring
+ * @property string|\Recurr\Rule $recurrence => \jamesiarmes\PhpEws\Type\RecurrenceType:Recurrence
+ * @property-read boolean $isRecurring => IsRecurring
* @property boolean $isAllDay => IsAllDayEvent
* @property boolean $isCancelled => IsCancelled
* @property boolean $isOnline => IsOnlineMeeting
* @property string $status => LegacyFreeBusyStatus
* @property-read string|\DateTime|integer $createdAt => DateTimeCreated
* @property-read string|\DateTime|integer $updatedAt => LastModifiedTime
+ * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment
*
* @property Contact $organizer => \jamesiarmes\PhpEws\Type\SingleRecipientType:Organizer
* @property Attendee[] $requiredAttendees => \jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType:RequiredAttendees.Attendee
@@ -85,11 +89,25 @@ public function rules(): array
]
],
+ ['recurrence', 'safe'],
+
['status', 'default', 'value' => LegacyFreeBusyType::BUSY],
['format', 'default', 'value' => BodyTypeType::HTML]
];
}
+ /**
+ * {@inheritDoc}
+ */
+ public function beforeSave($insert): bool
+ {
+ if ($this->isAttributeChanged('body', false)) {
+ $this->markAttributeDirty('format');
+ }
+
+ return parent::beforeSave($insert);
+ }
+
/**
* {@inheritDoc}
*/
@@ -106,7 +124,15 @@ public function behaviors(): array
'start' => [$this, 'typeCastDateTime'],
'end' => [$this, 'typeCastDateTime'],
'createdAt' => [$this, 'typeCastDateTime'],
- 'updatedAt' => [$this, 'typeCastDateTime']
+ 'updatedAt' => [$this, 'typeCastDateTime'],
+ 'recurrence' => function (string|RecurrenceType|null|array $value): ?Rule {
+ if (empty($value)) {
+ return null;
+ }
+
+ $transformer = new ExchangeTransformer();
+ return $transformer->transformRecurrenceFromEws($value);
+ }
]
]
];
diff --git a/src/models/Contact.php b/src/models/Contact.php
index db39a3a..3c5612b 100644
--- a/src/models/Contact.php
+++ b/src/models/Contact.php
@@ -19,8 +19,8 @@
* @property string $changeKey => \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.ChangeKey
* @property string $parentFolderId => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.Id
* @property string $parentFolderChangeKey => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.ChangeKey
- * @property string $email => Mailbox.EmailAddress
- * @property string $name => Mailbox.Name
+ * @property string $email => \jamesiarmes\PhpEws\Type\EmailAddressType:Mailbox.EmailAddress
+ * @property string $name => \jamesiarmes\PhpEws\Type\EmailAddressType:Mailbox.Name
*/
class Contact extends ActiveRecord
{
diff --git a/src/models/Message.php b/src/models/Message.php
index 7eed0e1..c0c65c7 100644
--- a/src/models/Message.php
+++ b/src/models/Message.php
@@ -31,7 +31,7 @@
* @property-read string|\DateTime|integer $createdAt => DateTimeCreated
* @property-read string|\DateTime|integer $updatedAt => LastModifiedTime
*
- * @property Attachment[] $attachments => \jamesiarmes\PhpEws\Type\ArrayOfAttachmentsType:Attachments.FileAttachment
+ * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment
* @property Contact $from => \jamesiarmes\PhpEws\Type\SingleRecipientType:From
* @property Contact $sender => \jamesiarmes\PhpEws\Type\SingleRecipientType:Sender
* @property Contact[] $to => \jamesiarmes\PhpEws\Type\ArrayOfRecipientsType:ToRecipients.Mailbox
diff --git a/src/models/Task.php b/src/models/Task.php
new file mode 100644
index 0000000..1694cd3
--- /dev/null
+++ b/src/models/Task.php
@@ -0,0 +1,194 @@
+ \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.Id
+ * @property string $changeKey => \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.ChangeKey
+ * @property string $parentFolderId => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.Id
+ * @property integer $actualWork => ActualWork
+ * @property string $assignedTime => AssignedTime
+ * @property string $billingInformation => BillingInformation
+ * @property string $body => \jamesiarmes\PhpEws\Type\BodyType:Body._
+ * @property integer $changeCount => ChangeCount
+ * @property string|\DateTime|integer $completeDate => CompleteDate
+ * @property-read string $delegationState => DelegationState
+ * @property string|\DateTime|integer $dueDate => DueDate
+ * @property string $format => \jamesiarmes\PhpEws\Type\BodyType:Body.BodyType
+ * @property string $importance => Importance
+ * @property-read integer $isAssignmentEditable => IsAssignmentEditable
+ * @property boolean $isComplete => IsComplete
+ * @property-read boolean $isRecurring => IsRecurring
+ * @property string $mileage => Mileage
+ * @property-read string $owner => Owner
+ * @property float $percentComplete => PercentComplete
+ * @property string|\Recurr\Rule $recurrence => \jamesiarmes\PhpEws\Type\TaskRecurrenceType:Recurrence
+ * @property string $sensitivity => Sensitivity
+ * @property string|\DateTime|integer $startDate => StartDate
+ * @property string $status => Status
+ * @property-read string $statusDescription => StatusDescription
+ * @property string $subject => Subject
+ * @property integer $totalWork => TotalWork
+ * @property-read string|\DateTime|integer $createdAt => DateTimeCreated
+ * @property-read string|\DateTime|integer $updatedAt => LastModifiedTime
+ *
+ * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment
+ */
+class Task extends ActiveRecord
+{
+ /**
+ * {@inheritDoc}
+ */
+ public static function modelName(): string
+ {
+ return TaskType::class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function rules(): array
+ {
+ return [
+ [
+ [
+ 'id',
+ 'assignedTime',
+ 'billingInformation',
+ 'body',
+ 'changeKey',
+ 'delegationState',
+ 'format',
+ 'importance',
+ 'mileage',
+ 'owner',
+ 'parentFolderId',
+ 'statusDescription',
+ 'sensitivity',
+ 'status',
+ 'subject'
+ ],
+ 'string'
+ ],
+
+ [['actualWork', 'changeCount', 'isAssignmentEditable', 'totalWork'], 'integer'],
+
+ [['percentComplete'], 'double'],
+
+ [['startDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'startDate'],
+ [['dueDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'dueDate'],
+ [['completeDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'completeDate'],
+
+ [['isComplete', 'isRecurring'], 'boolean'],
+
+ [['subject', 'format'], 'required'],
+
+ ['format', 'in', 'range' => [
+ BodyTypeType::HTML,
+ BodyTypeType::TEXT
+ ]],
+ [
+ 'delegationState',
+ 'in',
+ 'range' => [
+ TaskDelegateStateType::ACCEPTED,
+ TaskDelegateStateType::DECLINED,
+ TaskDelegateStateType::MAX,
+ TaskDelegateStateType::NO_MATCH,
+ TaskDelegateStateType::OWN_NEW,
+ TaskDelegateStateType::OWNED
+ ]
+ ],
+ [
+ 'status',
+ 'in',
+ 'range' => [
+ TaskStatusType::COMPLETED,
+ TaskStatusType::DEFERRED,
+ TaskStatusType::IN_PROGRESS,
+ TaskStatusType::NOT_STARTED,
+ TaskStatusType::WAITING_ON_OTHERS
+ ]
+ ],
+ [
+ 'importance',
+ 'in',
+ 'range' => [
+ ImportanceChoicesType::HIGH,
+ ImportanceChoicesType::LOW,
+ ImportanceChoicesType::NORMAL
+ ]
+ ],
+ [
+ 'sensitivity',
+ 'in',
+ 'range' => [
+ SensitivityChoicesType::CONFIDENTIAL,
+ SensitivityChoicesType::NORMAL,
+ SensitivityChoicesType::PERSONAL,
+ SensitivityChoicesType::PRIVATE_ITEM
+ ]
+ ],
+
+ ['recurrence', 'safe'],
+
+ ['format', 'default', 'value' => BodyTypeType::HTML]
+ ];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function beforeSave($insert): bool
+ {
+ if ($this->isAttributeChanged('body')) {
+ $this->markAttributeDirty('format');
+ }
+
+ return parent::beforeSave($insert);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function behaviors(): array
+ {
+ return [
+ 'typecast' => [
+ 'class' => AttributeTypecastBehavior::class,
+ 'typecastAfterSave' => false,
+ 'typecastAfterValidate' => false,
+ 'typecastBeforeSave' => false,
+ 'typecastAfterFind' => true,
+ 'attributeTypes' => [
+ 'assignedTime' => [$this, 'typeCastDateTime'],
+ 'completeDate' => [$this, 'typeCastDateTime'],
+ 'dueDate' => [$this, 'typeCastDateTime'],
+ 'startDate' => [$this, 'typeCastDateTime'],
+ 'createdAt' => [$this, 'typeCastDateTime'],
+ 'updatedAt' => [$this, 'typeCastDateTime'],
+ 'recurrence' => function (string|RecurrenceType|null|array $value): ?Rule {
+ if (empty($value)) {
+ return null;
+ }
+
+ $transformer = new ExchangeTransformer();
+ return $transformer->transformRecurrenceFromEws($value);
+ }
+ ]
+ ]
+ ];
+ }
+}
diff --git a/src/recurrence/transformers/ExchangeTransformer.php b/src/recurrence/transformers/ExchangeTransformer.php
new file mode 100644
index 0000000..920c749
--- /dev/null
+++ b/src/recurrence/transformers/ExchangeTransformer.php
@@ -0,0 +1,264 @@
+getInterval();
+ $count = $rule->getCount();
+ $until = $rule->getUntil();
+ $startDate = $rule->getStartDate() ?? 'today';
+ $endDate = $rule->getEndDate();
+
+ switch ($rule->getFreq()) {
+ case Frequency::YEARLY:
+ $byMonthDay = $rule->getByMonthDay();
+ $byMonth = $this->partAsString($rule->getByMonth());
+ if (!empty($byMonthDay)) {
+ $byMonthDay = $this->partAsString($byMonthDay);
+ $recurrence->AbsoluteYearlyRecurrence = new AbsoluteYearlyRecurrencePatternType();
+ $recurrence->AbsoluteYearlyRecurrence->Month = $byMonth;
+ $recurrence->AbsoluteYearlyRecurrence->DayOfMonth = $byMonthDay;
+ } else {
+ $byDay = $this->partAsString($rule->getByDay());
+ $byDayInt = (int)preg_replace('#[^\-+\d]#', '', $byDay);
+ $byDayString = preg_replace('#[\-+\d]#', '', $byDay);
+ $recurrence->RelativeYearlyRecurrence = new RelativeYearlyRecurrencePatternType();
+ $recurrence->RelativeYearlyRecurrence->Month = $byMonth;
+ $recurrence->RelativeYearlyRecurrence->DayOfWeekIndex = $this->weekDayIndexToString($byDayInt);
+ $recurrence->RelativeYearlyRecurrence->DaysOfWeek = $this->abbrToWeekDay($byDayString);
+ }
+ break;
+ case Frequency::MONTHLY:
+ $byMonthDay = $rule->getByMonthDay();
+ if (!empty($byMonthDay)) {
+ $byMonthDay = $this->partAsString($byMonthDay);
+ $recurrence->AbsoluteMonthlyRecurrence = new AbsoluteMonthlyRecurrencePatternType();
+ $recurrence->AbsoluteMonthlyRecurrence->DayOfMonth = $byMonthDay;
+ $recurrence->AbsoluteMonthlyRecurrence->Interval = $interval;
+ } else {
+ $byDay = $this->partAsString($rule->getByDay());
+ $byDayInt = (int)preg_replace('#[^\-\d]#', '', $byDay);
+ $byDayString = preg_replace('#[\-\d]#', '', $byDay);
+ $recurrence->RelativeMonthlyRecurrence = new RelativeMonthlyRecurrencePatternType();
+ $recurrence->RelativeMonthlyRecurrence->DayOfWeekIndex = $this->weekDayIndexToString($byDayInt);
+ $recurrence->RelativeMonthlyRecurrence->DaysOfWeek = $this->abbrToWeekDay($byDayString);
+ $recurrence->RelativeMonthlyRecurrence->Interval = $interval;
+ }
+ break;
+ case Frequency::WEEKLY:
+ $byDay = array_map(function (string $item) {
+ return $this->abbrToWeekDay($item);
+ }, (array)$rule->getByDay());
+ $recurrence->WeeklyRecurrence = new WeeklyRecurrencePatternType();
+ $recurrence->WeeklyRecurrence->DaysOfWeek = implode(' ', $byDay);
+ $recurrence->WeeklyRecurrence->Interval = $interval;
+ break;
+ case Frequency::DAILY:
+ $recurrence->DailyRecurrence = new DailyRecurrencePatternType();
+ $recurrence->DailyRecurrence->Interval = $interval;
+ break;
+ default:
+ return null;
+ }
+
+ if ($count) {
+ $recurrence->NumberedRecurrence = new NumberedRecurrenceRangeType();
+ $recurrence->NumberedRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx');
+ $recurrence->NumberedRecurrence->NumberOfOccurrences = $count;
+ } elseif ($endDate || $until) {
+ $recurrence->EndDateRecurrence = new EndDateRecurrenceRangeType();
+ $recurrence->EndDateRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx');
+ $recurrence->EndDateRecurrence->EndDate = Yii::$app->formatter->asDatetime($endDate ?? $until, 'yyyy-MM-dd\'T\'HH:mm:ssxxx');
+ } else {
+ $recurrence->NoEndRecurrence = new NoEndRecurrenceRangeType();
+ $recurrence->NoEndRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx');
+ }
+
+ return $recurrence;
+ }
+
+ /**
+ * Transform an EWS RecurrencyType array to a recurrence rule
+ * @param array $recurrence
+ * @return Rule
+ * @throws \Exception
+ */
+ public function transformRecurrenceFromEws(array $recurrence): Rule
+ {
+ $rule = new Rule();
+
+ // Start and end dates
+ if (isset($recurrence['NoEndRecurrence'])) {
+ $startDate = $recurrence['NoEndRecurrence']['StartDate'];
+ } elseif (isset($recurrence['EndDateRecurrence'])) {
+ $startDate = $recurrence['EndDateRecurrence']['StartDate'];
+ $rule->setUntil(new \DateTime($recurrence['EndDateRecurrence']['EndDate']));
+ } elseif (isset($recurrence['NumberedRecurrence'])) {
+ $startDate = $recurrence['NumberedRecurrence']['StartDate'];
+ $rule->setCount($recurrence['NumberedRecurrence']['NumberOfOccurrences']);
+ } else {
+ throw new InvalidConfigException('No end date or count given');
+ }
+ $rule->setStartDate(new \DateTime($startDate));
+
+
+ // Intervals
+
+ //daily
+ if (isset($recurrence['DailyRecurrence'])) {
+ $rule->setFreq(Frequency::DAILY);
+ $rule->setInterval($recurrence['DailyRecurrence']['Interval']);
+ }
+
+ // weekly
+ if (isset($recurrence['WeeklyRecurrence'])) {
+ $rule->setFreq(Frequency::WEEKLY);
+ $rule->setInterval($recurrence['WeeklyRecurrence']['Interval']);
+ $days = explode(' ', $recurrence['WeeklyRecurrence']['DaysOfWeek']);
+ $d = [];
+ foreach ($days as $day) {
+ $d[] = $this->weekDayToAbbr($day);
+ }
+ $rule->setByDay($d);
+ }
+
+ // absolute monthly
+ if (isset($recurrence['AbsoluteMonthlyRecurrence'])) {
+ $rule->setFreq(Frequency::MONTHLY);
+ $rule->setInterval($recurrence['AbsoluteMonthlyRecurrence']['Interval']);
+ $day = $recurrence['AbsoluteMonthlyRecurrence']['DayOfMonth'];
+ $rule->setByMonthDay([$day]);
+ }
+
+ // relative monthly
+ if (isset($recurrence['RelativeMonthlyRecurrence'])) {
+ $rule->setFreq(Frequency::MONTHLY);
+ $rule->setInterval($recurrence['RelativeMonthlyRecurrence']['Interval']);
+ $day = $this->weekDayToAbbr($recurrence['RelativeMonthlyRecurrence']['DaysOfWeek']);
+ $week = $this->stringToWeekDayIndex($recurrence['RelativeMonthlyRecurrence']['DayOfWeekIndex']);
+ $rule->setByDay([$week . $day]);
+ }
+
+ // absolute yearly
+ if (isset($recurrence['AbsoluteYearlyRecurrence'])) {
+ $rule->setFreq(Frequency::YEARLY);
+ $rule->setInterval(1);
+ $rule->setByMonth([date_parse($recurrence['AbsoluteYearlyRecurrence']['Month'])['month']]);
+ $rule->setByMonthDay([$recurrence['AbsoluteYearlyRecurrence']['DayOfMonth']]);
+ }
+
+ // relative yearly
+ if (isset($recurrence['RelativeYearlyRecurrence'])) {
+ $rule->setFreq(Frequency::YEARLY);
+ $rule->setInterval(1);
+ $rule->setByMonth([date_parse($recurrence['RelativeYearlyRecurrence']['Month'])['month']]);
+ $day = $this->weekDayToAbbr($recurrence['RelativeYearlyRecurrence']['DaysOfWeek']);
+ $week = $this->stringToWeekDayIndex($recurrence['RelativeYearlyRecurrence']['DayOfWeekIndex']);
+ $rule->setByDay([$week . $day]);
+ }
+
+ return $rule;
+ }
+
+ /**
+ * @param $string string The string to convert
+ * @return int
+ */
+ private function stringToWeekDayIndex(string $string): int
+ {
+ return match ($string) {
+ DayOfWeekIndexType::SECOND => 2,
+ DayOfWeekIndexType::THIRD => 3,
+ DayOfWeekIndexType::FOURTH => 4,
+ DayOfWeekIndexType::LAST => -1,
+ default => 1
+ };
+ }
+
+ /**
+ * @param int $index The index to convert
+ * @return string
+ */
+ private function weekDayIndexToString(int $index): string
+ {
+
+ return match ($index) {
+ 2 => DayOfWeekIndexType::SECOND,
+ 3 => DayOfWeekIndexType::THIRD,
+ 4 => DayOfWeekIndexType::FOURTH,
+ -1 => DayOfWeekIndexType::LAST,
+ default => DayOfWeekIndexType::FIRST
+ };
+ }
+
+ /**
+ * @param $weekday
+ * @return string
+ */
+ private function weekDayToAbbr($weekday): string
+ {
+ return match ($weekday) {
+ DayOfWeekType::TUESDAY => 'TU',
+ DayOfWeekType::WEDNESDAY => 'WE',
+ DayOfWeekType::THURSDAY => 'TH',
+ DayOfWeekType::FRIDAY => 'FR',
+ DayOfWeekType::SATURDAY => 'SA',
+ DayOfWeekType::SUNDAY => 'SU',
+ default => 'MO'
+ };
+ }
+
+ /**
+ * @param $abbr
+ * @return string
+ */
+ private function abbrToWeekDay($abbr): string
+ {
+ return match ($abbr) {
+ 'TU' => DayOfWeekType::TUESDAY,
+ 'WE' => DayOfWeekType::WEDNESDAY,
+ 'TH' => DayOfWeekType::THURSDAY,
+ 'FR' => DayOfWeekType::FRIDAY,
+ 'SA' => DayOfWeekType::SATURDAY,
+ 'SU' => DayOfWeekType::SUNDAY,
+ default => DayOfWeekType::MONDAY
+ };
+ }
+
+ private function partAsString(array|string $part): string
+ {
+ if (is_array($part) && !ArrayHelper::isAssociative($part) && isset($part[0])) {
+ return $part[0];
+ }
+
+ return (string)$part;
+ }
+}
diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php
new file mode 100644
index 0000000..2ba84d4
--- /dev/null
+++ b/tests/AttachmentTest.php
@@ -0,0 +1,107 @@
+ [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType',
+ 'foreignField' => 'AttachmentId.Id'
+ ],
+ 'rootId' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType',
+ 'foreignField' => 'AttachmentId.RootItemId'
+ ],
+ 'rootChangeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType',
+ 'foreignField' => 'AttachmentId.RootItemChangeKey'
+ ],
+ 'contentId' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ContentId'
+ ],
+ 'location' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ContentLocation'
+ ],
+ 'mime' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ContentType'
+ ],
+ 'isInline' => [
+ 'readOnly' => false,
+ 'dataType' => ['boolean'],
+ 'foreignModel' => null,
+ 'foreignField' => 'IsInline'
+ ],
+ 'name' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Name'
+ ],
+ 'size' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Size'
+ ],
+ 'content' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FileAttachmentType',
+ 'foreignField' => 'Content'
+ ],
+ 'isContactPhoto' => [
+ 'readOnly' => false,
+ 'dataType' => ['boolean'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FileAttachmentType',
+ 'foreignField' => 'IsContactPhoto'
+ ],
+ ];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
+ foreach ($expectedSubset as $key => $value) {
+ $this->assertArrayHasKey($key, $attributeMapping);
+ $this->assertSame($value, $attributeMapping[$key]);
+ }
+ }
+
+ public function testModel()
+ {
+ $file = b'This is a text file';
+ $attachment = new Attachment([
+ 'name' => 'test',
+ 'content' => $file,
+ 'mime' => 'text/plain',
+ 'isInline' => false,
+ 'size' => strlen($file)
+ ]);
+
+ $this->assertTrue($attachment->validate());
+ }
+}
diff --git a/tests/AttendeeTest.php b/tests/AttendeeTest.php
new file mode 100644
index 0000000..865b4a7
--- /dev/null
+++ b/tests/AttendeeTest.php
@@ -0,0 +1,49 @@
+ [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType',
+ 'foreignField' => 'Mailbox.Name'
+ ],
+ 'email' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType',
+ 'foreignField' => 'Mailbox.EmailAddress'
+ ],
+ ];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
+ foreach ($expectedSubset as $key => $value) {
+ $this->assertArrayHasKey($key, $attributeMapping);
+ $this->assertSame($value, $attributeMapping[$key]);
+ }
+ }
+
+ public function testModel()
+ {
+ $attendee = new Attendee([
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.com'
+ ]);
+ $this->assertTrue(true, $attendee->validate());
+ $this->assertEquals('jamesiarmes\PhpEws\Type\AttendeeType', Attendee::modelName());
+ }
+}
diff --git a/tests/CalendarEventTest.php b/tests/CalendarEventTest.php
index 896c7bd..822cb3d 100644
--- a/tests/CalendarEventTest.php
+++ b/tests/CalendarEventTest.php
@@ -6,119 +6,182 @@
namespace yiiunit\extensions\ews;
+use jamesiarmes\PhpEws\Enumeration\BodyTypeType;
use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
use jamesiarmes\PhpEws\Enumeration\DisposalType;
use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType;
use jamesiarmes\PhpEws\Enumeration\LegacyFreeBusyType;
use jamesiarmes\PhpEws\Enumeration\SortDirectionType;
use jamesiarmes\PhpEws\Enumeration\UnindexedFieldURIType;
+use simialbi\yii2\ews\models\Attachment;
use simialbi\yii2\ews\models\Attendee;
use simialbi\yii2\ews\models\CalendarEvent;
use Yii;
+use yii\base\InvalidConfigException;
+use yii\base\NotSupportedException;
class CalendarEventTest extends TestCase
{
+ /**
+ * @throws \ReflectionException
+ */
public function testAttributeMapping()
{
$attributeMapping = CalendarEvent::attributeMapping();
$expectedSubset = [
'id' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
'foreignField' => 'ItemId.Id'
],
'changeKey' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
'foreignField' => 'ItemId.ChangeKey'
],
'parentFolderId' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
'foreignField' => 'ParentFolderId.Id'
],
'parentFolderChangeKey' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
'foreignField' => 'ParentFolderId.ChangeKey'
],
'start' => [
+ 'readOnly' => false,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'Start'
],
'end' => [
+ 'readOnly' => false,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'End'
],
'subject' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'Subject'
],
'body' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
'foreignField' => 'Body._'
],
'format' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
'foreignField' => 'Body.BodyType'
],
'location' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'Location'
],
'type' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'CalendarItemType'
],
'isRecurring' => [
+ 'readOnly' => true,
'dataType' => ['boolean'],
'foreignModel' => null,
'foreignField' => 'IsRecurring'
],
'isAllDay' => [
+ 'readOnly' => false,
'dataType' => ['boolean'],
'foreignModel' => null,
'foreignField' => 'IsAllDayEvent'
],
'isCancelled' => [
+ 'readOnly' => false,
'dataType' => ['boolean'],
'foreignModel' => null,
'foreignField' => 'IsCancelled'
],
'isOnline' => [
+ 'readOnly' => false,
'dataType' => ['boolean'],
'foreignModel' => null,
'foreignField' => 'IsOnlineMeeting'
],
'status' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'LegacyFreeBusyStatus'
],
'createdAt' => [
+ 'readOnly' => true,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'DateTimeCreated'
],
'updatedAt' => [
+ 'readOnly' => true,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'LastModifiedTime'
],
+ 'recurrence' => [
+ 'readOnly' => false,
+ 'dataType' => ['string', '\Recurr\Rule'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\RecurrenceType',
+ 'foreignField' => 'Recurrence'
+ ],
+ 'organizer' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType',
+ 'foreignField' => 'Organizer'
+ ],
+ 'requiredAttendees' => [
+ 'readOnly' => false,
+ 'dataType' => ['Attendee[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType',
+ 'foreignField' => 'RequiredAttendees.Attendee'
+ ],
+ 'optionalAttendees' => [
+ 'readOnly' => false,
+ 'dataType' => ['Attendee[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType',
+ 'foreignField' => 'OptionalAttendees.Attendee'
+ ],
+ 'attachments' => [
+ 'readOnly' => false,
+ 'dataType' => ['Attachment[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType',
+ 'foreignField' => 'Attachments.FileAttachment'
+ ]
];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
foreach ($expectedSubset as $key => $value) {
$this->assertArrayHasKey($key, $attributeMapping);
$this->assertSame($value, $attributeMapping[$key]);
}
}
+ /**
+ * @throws InvalidConfigException
+ */
public function testQueryBuilderFind()
{
$query = CalendarEvent::find();
@@ -168,6 +231,11 @@ public function testQueryBuilderFind()
$this->assertEquals(SortDirectionType::ASCENDING, $request->SortOrder->FieldOrder[0]->Order);
}
+ /**
+ * @throws NotSupportedException
+ * @throws InvalidConfigException
+ * @throws \ReflectionException
+ */
public function testQueryBuilderInsert()
{
$startDate = Yii::$app->formatter->asDate('+2 hours', 'yyyy-MM-dd HH:mm xxx');
@@ -176,14 +244,30 @@ public function testQueryBuilderInsert()
$event->subject = 'Test';
$event->body = '
This is a test
';
+ $event->format = BodyTypeType::HTML;
$event->start = $startDate;
$event->end = $endDate;
$event->requiredAttendees = [
new Attendee(['name' => 'John Doe', 'email' => 'john.doe@example.com']),
new Attendee(['name' => 'Jane Doe', 'email' => 'jane.doe@example.com'])
];
+ $event->recurrence = 'FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=20;BYMONTH=11';
- $this->assertEquals(true, $event->validate());
+ $file = b'This is a text file';
+ $attachments = [
+ new Attachment([
+ 'name' => 'test',
+ 'content' => $file,
+ 'mime' => 'text/plain',
+ 'isInline' => false,
+ 'size' => strlen($file)
+ ])
+ ];
+ $event->attachments = $attachments;
+
+ $event->beforeSave(true);
+
+ $this->assertTrue($event->validate());
$values = $event->getDirtyAttributes();
$params = [
@@ -213,11 +297,27 @@ public function testQueryBuilderInsert()
$this->assertEquals('This is a test
', $calendarItem->Body->_);
$this->assertEquals('Test', $calendarItem->Subject);
$this->assertInstanceOf('jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType', $calendarItem->RequiredAttendees);
+
+ // Attendee
$attendee = $calendarItem->RequiredAttendees->Attendee[0];
$this->assertInstanceOf('jamesiarmes\PhpEws\Type\AttendeeType', $attendee);
$this->assertInstanceOf('jamesiarmes\PhpEws\Type\EmailAddressType', $attendee->Mailbox);
$this->assertEquals('john.doe@example.com', $attendee->Mailbox->EmailAddress);
$this->assertEquals('John Doe', $attendee->Mailbox->Name);
+
+ // Recurrence
+ $this->assertInstanceOf('jamesiarmes\PhpEws\Type\RecurrenceType', $calendarItem->Recurrence);
+ $this->assertIsObject($calendarItem->Recurrence);
+ $this->assertIsObject($calendarItem->Recurrence->NoEndRecurrence);
+
+ // Attachments
+ $attachment = $calendarItem->Attachments->FileAttachment[0];
+ $this->assertObjectHasProperty('Attachments', $calendarItem);
+ $this->assertInstanceOf('jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType', $calendarItem->Attachments);
+ $this->assertObjectHasProperty('FileAttachment', $calendarItem->Attachments);
+ $this->assertInstanceOf('jamesiarmes\PhpEws\Type\AttachmentType', $attachment);
+ $this->assertEquals('text/plain', $attachment->ContentType);
+ $this->assertEquals(19, $attachment->Size);
}
public function testQueryBuilderUpdate()
@@ -228,6 +328,7 @@ public function testQueryBuilderUpdate()
$event->subject = 'Test';
$event->body = 'This is a test
';
+ $event->format = 'HTML';
$event->start = $startDate;
$event->end = $endDate;
$event->requiredAttendees = [
@@ -275,6 +376,7 @@ public function testQueryBuilderUpdate()
$this->assertEquals(date('c', strtotime($endDate)), $updates->SetItemField[3]->CalendarItem->End);
$this->assertEquals(UnindexedFieldURIType::CALENDAR_LEGACY_FREE_BUSY_STATUS, $updates->SetItemField[4]->FieldURI->FieldURI);
$this->assertEquals(LegacyFreeBusyType::BUSY, $updates->SetItemField[4]->CalendarItem->LegacyFreeBusyStatus);
+ $this->assertObjectHasProperty('SendMeetingInvitationsOrCancellations', $request);
}
public function testQueryBuilderDelete()
@@ -293,5 +395,6 @@ public function testQueryBuilderDelete()
$this->assertInstanceOf('jamesiarmes\PhpEws\Type\ItemIdType', $request->ItemIds->ItemId[0]);
$this->assertEquals('7007ACC7-3202-11D1-AAD2-00805FC1270E', $request->ItemIds->ItemId[0]->ChangeKey);
$this->assertEquals('AAajslgkha32394isdg==', $request->ItemIds->ItemId[0]->Id);
+ $this->assertObjectHasProperty('SendMeetingCancellations', $request);
}
}
diff --git a/tests/ContactTest.php b/tests/ContactTest.php
new file mode 100644
index 0000000..f64af55
--- /dev/null
+++ b/tests/ContactTest.php
@@ -0,0 +1,81 @@
+ [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.Id'
+ ],
+ 'changeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.ChangeKey'
+ ],
+ 'parentFolderId' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
+ 'foreignField' => 'ParentFolderId.Id'
+ ],
+ 'parentFolderChangeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
+ 'foreignField' => 'ParentFolderId.ChangeKey'
+ ],
+ 'name' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType',
+ 'foreignField' => 'Mailbox.Name'
+ ],
+ 'email' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType',
+ 'foreignField' => 'Mailbox.EmailAddress'
+ ],
+ ];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
+ foreach ($expectedSubset as $key => $value) {
+ $this->assertArrayHasKey($key, $attributeMapping);
+ $this->assertSame($value, $attributeMapping[$key]);
+ }
+ }
+
+ public function testModel()
+ {
+ $contact = new Contact([
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.com'
+ ]);
+ $this->assertTrue($contact->validate());
+
+ $recipient = new SingleRecipientType();
+ $recipient->Mailbox = new EmailAddressType();
+ $recipient->Mailbox->EmailAddress = 'john.doe@example.com';
+ $recipient->Mailbox->Name = 'John Doe';
+ $contact = Contact::fromSingleRecipient($recipient);
+ self::assertTrue($contact->validate());
+ }
+}
diff --git a/tests/EwsToRuleTest.php b/tests/EwsToRuleTest.php
new file mode 100644
index 0000000..ec4b4b8
--- /dev/null
+++ b/tests/EwsToRuleTest.php
@@ -0,0 +1,221 @@
+
+ */
+
+namespace yiiunit\extensions\ews;
+
+use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer;
+
+class EwsToRuleTest extends TestCase
+{
+ /**
+ * @throws \Exception
+ */
+ public function testDaily()
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => null,
+ 'AbsoluteYearlyRecurrence' => null,
+ 'DailyRecurrence' => [
+ 'Interval' => 5
+ ],
+ 'EndDateRecurrence' => [
+ 'StartDate' => '2025-11-17+01:00',
+ 'EndDate' => '2026-02-09+01:00'
+ ],
+ 'NoEndRecurrence' => null,
+ 'NumberedRecurrence' => null,
+ 'RelativeMonthlyRecurrence' => null,
+ 'RelativeYearlyRecurrence' => null,
+ 'WeeklyRecurrence' => null,
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null
+ ];
+
+ $this->assertEquals(
+ 'FREQ=DAILY;UNTIL=20260209T000000;INTERVAL=5',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testeWeekly()
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => null,
+ 'AbsoluteYearlyRecurrence' => null,
+ 'DailyRecurrence' => null,
+ 'EndDateRecurrence' => [
+ 'StartDate' => '2025-11-17+01:00',
+ 'EndDate' => '2026-05-04+02:00'
+ ],
+ 'NoEndRecurrence' => null,
+ 'NumberedRecurrence' => null,
+ 'RelativeMonthlyRecurrence' => null,
+ 'RelativeYearlyRecurrence' => null,
+ 'WeeklyRecurrence' => [
+ 'Interval' => 2,
+ 'DaysOfWeek' => 'Monday Saturday',
+ 'FirstDayOfWeek' => 'Sunday'
+ ],
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null
+ ];
+
+ $this->assertEquals(
+ 'FREQ=WEEKLY;UNTIL=20260504T000000;INTERVAL=2;BYDAY=MO,SA',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testAbsoluteMonthly()
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => [
+ 'Interval' => 2,
+ 'DayOfMonth' => 17
+ ],
+ 'AbsoluteYearlyRecurrence' => null,
+ 'DailyRecurrence' => null,
+ 'EndDateRecurrence' => null,
+ 'NoEndRecurrence' => null,
+ 'NumberedRecurrence' => [
+ 'StartDate' => '2025-11-17+01:00',
+ 'NumberOfOccurrences' => 7
+ ],
+ 'RelativeMonthlyRecurrence' => null,
+ 'RelativeYearlyRecurrence' => null,
+ 'WeeklyRecurrence' => null,
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null
+ ];
+
+ $this->assertEquals(
+ 'FREQ=MONTHLY;COUNT=7;INTERVAL=2;BYMONTHDAY=17',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testRelativeMonthly()
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => null,
+ 'AbsoluteYearlyRecurrence' => null,
+ 'DailyRecurrence' => null,
+ 'EndDateRecurrence' => null,
+ 'NoEndRecurrence' => [
+ 'StartDate' => '2025-11-17+01:00'
+ ],
+ 'NumberedRecurrence' => null,
+ 'RelativeMonthlyRecurrence' => [
+ 'Interval' => 2,
+ 'DayOfWeekIndex' => 'Third',
+ 'DaysOfWeek' => 'Thursday'
+ ],
+ 'RelativeYearlyRecurrence' => null,
+ 'WeeklyRecurrence' => null,
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null
+ ];
+
+ $this->assertEquals(
+ 'FREQ=MONTHLY;INTERVAL=2;BYDAY=3TH',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testAbsoluteYearly(): void
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => null,
+ 'AbsoluteYearlyRecurrence' => [
+ 'DayOfMonth' => 20,
+ 'Month' => 'November'
+ ],
+ 'DailyRecurrence' => null,
+ 'EndDateRecurrence' => null,
+ 'NoEndRecurrence' => [
+ 'StartDate' => '2023-11-20+01:00'
+ ],
+ 'NumberedRecurrence' => null,
+ 'RelativeMonthlyRecurrence' => null,
+ 'RelativeYearlyRecurrence' => null,
+ 'WeeklyRecurrence' => null,
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null,
+ ];
+
+ $this->assertEquals(
+ 'FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=20;BYMONTH=11',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testRelativeYearly(): void
+ {
+ $data = [
+ 'AbsoluteMonthlyRecurrence' => null,
+ 'AbsoluteYearlyRecurrence' => null,
+ 'DailyRecurrence' => null,
+ 'EndDateRecurrence' => [
+ 'StartDate' => '2025-11-17+01:00',
+ 'EndDate' => '2032-11-30+01:00'
+ ],
+ 'NoEndRecurrence' => null,
+ 'NumberedRecurrence' => null,
+ 'RelativeMonthlyRecurrence' => null,
+ 'RelativeYearlyRecurrence' => [
+ 'DayOfWeekIndex' => 'Third',
+ 'DaysOfWeek' => 'Monday',
+ 'Month' => 'November',
+ ],
+ 'WeeklyRecurrence' => null,
+ 'DailyRegeneration' => null,
+ 'MonthlyRegeneration' => null,
+ 'WeeklyRegeneration' => null,
+ 'YearlyRegeneration' => null,
+ ];
+
+ $this->assertEquals(
+ 'FREQ=YEARLY;UNTIL=20321130T000000;INTERVAL=1;BYDAY=3MO;BYMONTH=11',
+ $this->getRule($data)->getString()
+ );
+ }
+
+ /**
+ * @param array $data
+ * @return \Recurr\Rule
+ * @throws \Exception
+ */
+ protected function getRule(array $data): \Recurr\Rule
+ {
+ $transformer = new ExchangeTransformer();
+ return $transformer->transformRecurrenceFromEws($data);
+ }
+}
diff --git a/tests/FolderTest.php b/tests/FolderTest.php
new file mode 100644
index 0000000..c339ac2
--- /dev/null
+++ b/tests/FolderTest.php
@@ -0,0 +1,84 @@
+ [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.Id'
+ ],
+ 'changeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.ChangeKey'
+ ],
+ 'parentFolderId' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
+ 'foreignField' => 'ParentFolderId.Id'
+ ],
+ 'parentFolderChangeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
+ 'foreignField' => 'ParentFolderId.ChangeKey'
+ ],
+ 'name' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'DisplayName'
+ ],
+ 'unreadCount' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'UnreadCount'
+ ],
+ 'totalCount' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'TotalCount'
+ ],
+ 'childrenCount' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ChildFolderCount'
+ ],
+ ];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
+ foreach ($expectedSubset as $key => $value) {
+ $this->assertArrayHasKey($key, $attributeMapping);
+ $this->assertSame($value, $attributeMapping[$key]);
+ }
+ }
+
+ public function testModel()
+ {
+ $folder = new Folder([
+ 'name' => 'Test Folder',
+ 'totalCount' => 3
+ ]);
+ $this->assertTrue($folder->validate());
+ }
+}
diff --git a/tests/MessageTest.php b/tests/MessageTest.php
index 260a182..0db4037 100644
--- a/tests/MessageTest.php
+++ b/tests/MessageTest.php
@@ -6,83 +6,163 @@
namespace yiiunit\extensions\ews;
+use jamesiarmes\PhpEws\Enumeration\BodyTypeType;
+use jamesiarmes\PhpEws\Enumeration\SensitivityChoicesType;
use simialbi\yii2\ews\models\Message;
class MessageTest extends TestCase
{
+ /**
+ * @throws \ReflectionException
+ */
public function testAttributeMapping()
{
$attributeMapping = Message::attributeMapping();
$expectedSubset = [
'id' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
'foreignField' => 'ItemId.Id'
],
'changeKey' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
'foreignField' => 'ItemId.ChangeKey'
],
'parentFolderId' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
'foreignField' => 'ParentFolderId.Id'
],
+ 'attachments' => [
+ 'readOnly' => false,
+ 'dataType' => ['Attachment[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType',
+ 'foreignField' => 'Attachments.FileAttachment'
+ ],
'sensitivity' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'Sensitivity'
],
'importance' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'Importance'
],
'subject' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'Subject'
],
'format' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
'foreignField' => 'Body.BodyType'
],
'body' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
'foreignField' => 'Body._'
],
'messageId' => [
+ 'readOnly' => false,
'dataType' => ['string'],
'foreignModel' => null,
'foreignField' => 'InternetMessageId'
],
'isRead' => [
+ 'readOnly' => false,
'dataType' => ['boolean'],
'foreignModel' => null,
'foreignField' => 'IsRead'
],
'sentAt' => [
+ 'readOnly' => true,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'DateTimeSent'
],
'createdAt' => [
+ 'readOnly' => true,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'DateTimeCreated'
],
'updatedAt' => [
+ 'readOnly' => true,
'dataType' => ['string', '\DateTime', 'integer'],
'foreignModel' => null,
'foreignField' => 'LastModifiedTime'
],
+ 'from' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType',
+ 'foreignField' => 'From'
+ ],
+ 'sender' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType',
+ 'foreignField' => 'Sender'
+ ],
+ 'to' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType',
+ 'foreignField' => 'ToRecipients.Mailbox'
+ ],
+ 'cc' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType',
+ 'foreignField' => 'CcRecipients.Mailbox'
+ ],
+ 'bcc' => [
+ 'readOnly' => false,
+ 'dataType' => ['Contact[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType',
+ 'foreignField' => 'BccRecipients.Mailbox'
+ ],
];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
foreach ($expectedSubset as $key => $value) {
$this->assertArrayHasKey($key, $attributeMapping);
$this->assertSame($value, $attributeMapping[$key]);
}
}
+
+ public function testModel()
+ {
+ $message = new Message([
+ 'subject' => 'Test subject',
+ 'body' => 'Test body',
+ 'format' => BodyTypeType::TEXT,
+ 'isRead' => false,
+ 'sensitivity' => SensitivityChoicesType::CONFIDENTIAL,
+ 'from' => [
+ 'name' => 'Jane Doe',
+ 'email' => 'jane.doe@example.com'
+ ],
+ 'to' => [
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.com'
+ ]
+ ]);
+
+ $this->assertTrue($message->validate());
+ }
}
diff --git a/tests/RuleToEwsTest.php b/tests/RuleToEwsTest.php
new file mode 100644
index 0000000..eba97c0
--- /dev/null
+++ b/tests/RuleToEwsTest.php
@@ -0,0 +1,148 @@
+setFreq(Frequency::DAILY);
+ $rule->setUntil($endDate);
+ $rule->setInterval(5);
+ $rule->setStartDate($startDate);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals(5, $recurrenceType->DailyRecurrence->Interval);
+ $this->assertEquals($startDate->format('c'), $recurrenceType->EndDateRecurrence->StartDate);
+ $this->assertEquals($endDate->format('c'), $recurrenceType->EndDateRecurrence->EndDate);
+ }
+
+ /**
+ * @throws InvalidArgument
+ * @throws InvalidRRule
+ * @throws InvalidConfigException
+ */
+ public function testWeekly()
+ {
+ $startDate = new \DateTime();
+ $startDate->setTime(0, 0);
+
+ $rule = new Rule();
+ $rule->setFreq(Frequency::WEEKLY);
+ $rule->setCount(3);
+ $rule->setInterval(2);
+ $rule->setByDay(['MO', 'SA']);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals(2, $recurrenceType->WeeklyRecurrence->Interval);
+ $this->assertEquals(3, $recurrenceType->NumberedRecurrence->NumberOfOccurrences);
+ $this->assertEquals($startDate->format('c'), $recurrenceType->NumberedRecurrence->StartDate);
+ }
+
+ /**
+ * @throws InvalidConfigException
+ * @throws InvalidArgument
+ */
+ public function testAbsoluteMonthly()
+ {
+ $rule = new Rule();
+ $rule->setFreq(Frequency::MONTHLY);
+ $rule->setInterval(5);
+ $rule->setByMonthDay([13]);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals(5, $recurrenceType->AbsoluteMonthlyRecurrence->Interval);
+ $this->assertEquals(13, $recurrenceType->AbsoluteMonthlyRecurrence->DayOfMonth);
+ }
+
+ /**
+ * @throws InvalidArgument
+ * @throws InvalidRRule
+ * @throws InvalidConfigException
+ */
+ public function testRelativeMonth()
+ {
+ $rule = new Rule();
+ $rule->setFreq(Frequency::MONTHLY);
+ $rule->setInterval(2);
+ $rule->setByDay(['-1SU']);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals(2, $recurrenceType->RelativeMonthlyRecurrence->Interval);
+ $this->assertEquals('Last', $recurrenceType->RelativeMonthlyRecurrence->DayOfWeekIndex);
+ $this->assertEquals('Sunday', $recurrenceType->RelativeMonthlyRecurrence->DaysOfWeek);
+ }
+
+ /**
+ * @throws InvalidArgument
+ * @throws InvalidConfigException
+ */
+ public function testAbsoluteYear()
+ {
+ $rule = new Rule();
+ $rule->setFreq(Frequency::YEARLY);
+ $rule->setByMonthDay([12]);
+ $rule->setByMonth([11]);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals(12, $recurrenceType->AbsoluteYearlyRecurrence->DayOfMonth);
+ $this->assertEquals(11, $recurrenceType->AbsoluteYearlyRecurrence->Month);
+ }
+
+ /**
+ * @return void
+ * @throws InvalidRRule
+ * @throws InvalidArgument|InvalidConfigException
+ */
+ public function testRelativeYear()
+ {
+ $rule = new Rule();
+ $rule->setFreq(Frequency::YEARLY);
+ $rule->setByMonth([1]);
+ $rule->setByDay(['+1MO']);
+
+
+ $recurrenceType = $this->getRecurrenceType($rule);
+
+ $this->assertEquals('First', $recurrenceType->RelativeYearlyRecurrence->DayOfWeekIndex);
+ $this->assertEquals('Monday', $recurrenceType->RelativeYearlyRecurrence->DaysOfWeek);
+ $this->assertEquals(1, $recurrenceType->RelativeYearlyRecurrence->Month);
+ }
+
+ /**
+ * @param Rule $rule
+ * @return RecurrenceType|null
+ * @throws InvalidConfigException
+ */
+ protected function getRecurrenceType(Rule $rule): ?RecurrenceType
+ {
+ $t = new ExchangeTransformer();
+ return $t->transformRecurrenceToEws($rule);
+ }
+}
diff --git a/tests/TaskTest.php b/tests/TaskTest.php
new file mode 100644
index 0000000..6c94bea
--- /dev/null
+++ b/tests/TaskTest.php
@@ -0,0 +1,232 @@
+ [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.Id'
+ ],
+ 'changeKey' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType',
+ 'foreignField' => 'ItemId.ChangeKey'
+ ],
+ 'parentFolderId' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType',
+ 'foreignField' => 'ParentFolderId.Id'
+ ],
+ 'attachments' => [
+ 'readOnly' => false,
+ 'dataType' => ['Attachment[]'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType',
+ 'foreignField' => 'Attachments.FileAttachment'
+ ],
+ 'sensitivity' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Sensitivity'
+ ],
+ 'importance' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Importance'
+ ],
+ 'subject' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Subject'
+ ],
+ 'format' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
+ 'foreignField' => 'Body.BodyType'
+ ],
+ 'body' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType',
+ 'foreignField' => 'Body._'
+ ],
+ 'actualWork' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ActualWork'
+ ],
+ 'assignedTime' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'AssignedTime'
+ ],
+ 'billingInformation' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'BillingInformation'
+ ],
+ 'changeCount' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'ChangeCount'
+ ],
+ 'completeDate' => [
+ 'readOnly' => false,
+ 'dataType' => ['string', '\DateTime', 'integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'CompleteDate'
+ ],
+ 'delegationState' => [
+ 'readOnly' => true,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'DelegationState'
+ ],
+ 'dueDate' => [
+ 'readOnly' => false,
+ 'dataType' => ['string', '\DateTime', 'integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'DueDate'
+ ],
+ 'isAssignmentEditable' => [
+ 'readOnly' => true,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'IsAssignmentEditable'
+ ],
+ 'isComplete' => [
+ 'readOnly' => false,
+ 'dataType' => ['boolean'],
+ 'foreignModel' => null,
+ 'foreignField' => 'IsComplete'
+ ],
+ 'isRecurring' => [
+ 'readOnly' => true,
+ 'dataType' => ['boolean'],
+ 'foreignModel' => null,
+ 'foreignField' => 'IsRecurring'
+ ],
+ 'mileage' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Mileage'
+ ],
+ 'owner' => [
+ 'readOnly' => true,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Owner'
+ ],
+ 'percentComplete' => [
+ 'readOnly' => false,
+ 'dataType' => ['float'],
+ 'foreignModel' => null,
+ 'foreignField' => 'PercentComplete'
+ ],
+ 'recurrence' => [
+ 'readOnly' => false,
+ 'dataType' => ['string', '\Recurr\Rule'],
+ 'foreignModel' => '\jamesiarmes\PhpEws\Type\TaskRecurrenceType',
+ 'foreignField' => 'Recurrence'
+ ],
+ 'startDate' => [
+ 'readOnly' => false,
+ 'dataType' => ['string', '\DateTime', 'integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'StartDate'
+ ],
+ 'status' => [
+ 'readOnly' => false,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'Status'
+ ],
+ 'statusDescription' => [
+ 'readOnly' => true,
+ 'dataType' => ['string'],
+ 'foreignModel' => null,
+ 'foreignField' => 'StatusDescription'
+ ],
+ 'totalWork' => [
+ 'readOnly' => false,
+ 'dataType' => ['integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'TotalWork'
+ ],
+ 'createdAt' => [
+ 'readOnly' => true,
+ 'dataType' => ['string', '\DateTime', 'integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'DateTimeCreated'
+ ],
+ 'updatedAt' => [
+ 'readOnly' => true,
+ 'dataType' => ['string', '\DateTime', 'integer'],
+ 'foreignModel' => null,
+ 'foreignField' => 'LastModifiedTime'
+ ],
+ ];
+
+ foreach ($attributeMapping as $key => $value) {
+ $this->assertArrayHasKey($key, $expectedSubset);
+ }
+
+ foreach ($expectedSubset as $key => $value) {
+ $this->assertArrayHasKey($key, $attributeMapping);
+ $this->assertSame($value, $attributeMapping[$key]);
+ }
+ }
+
+ public function testModel()
+ {
+ $startDate = \Yii::$app->formatter->asDate('+2 hours', 'yyyy-MM-dd HH:mm xxx');
+ $endDate = \Yii::$app->formatter->asDate('+2.5 hours', 'yyyy-MM-dd HH:mm xxx');
+
+ $task = new Task([
+ 'body' => 'Test',
+ 'subject' => 'Test',
+ 'startDate' => $startDate,
+ 'dueDate' => $endDate,
+ 'percentComplete' => 0,
+ 'format' => BodyTypeType::HTML,
+ 'status' => TaskStatusType::NOT_STARTED
+ ]);
+ $this->assertTrue($task->validate());
+ }
+
+ public function testDelete()
+ {
+ $event = new Task();
+ $params = [];
+ $request = $event::getDb()->getQueryBuilder()->delete(get_class($event), [
+ 'id' => 'AAajslgkha32394isdg==',
+ 'changeKey' => '7007ACC7-3202-11D1-AAD2-00805FC1270E'
+ ], $params);
+
+ $this->assertObjectHasProperty('AffectedTaskOccurrences', $request);
+ }
+}