mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-30 07:34:37 +00:00
Enhance Zimbra integration in ChillZimbraBundle
- Added `TestCommand` for testing Zimbra API interactions. - Introduced `ZimbraIdSerializer` to handle serialization/deserialization of Zimbra calendar IDs. - Implemented `UpdateEvent` to support updating Zimbra calendar events. - Enhanced `CreateEvent` to return serialized Zimbra IDs. - Updated `ZimbraConnector` to handle creation and updating of calendar ranges with Zimbra. - Added `ZimbraCalendarIdNotDeserializedException` for error handling during ID deserialization.
This commit is contained in:
@@ -17,6 +17,7 @@ use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector\CreateEvent;
|
||||
use Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector\UpdateEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -26,6 +27,7 @@ final readonly class ZimbraConnector implements RemoteCalendarConnectorInterface
|
||||
|
||||
public function __construct(
|
||||
private CreateEvent $createEvent,
|
||||
private UpdateEvent $updateEvent,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -77,13 +79,13 @@ final readonly class ZimbraConnector implements RemoteCalendarConnectorInterface
|
||||
{
|
||||
if (!$calendarRange->hasRemoteId()) {
|
||||
$calItemId = ($this->createEvent)($calendarRange);
|
||||
$this->logger->info(self::LOG_PREFIX.'Calendar range synced with Zimbra', ['calendar_range_id' => $calendarRange->getId(), 'calItemId' => $calItemId]);
|
||||
$this->logger->info(self::LOG_PREFIX.'Calendar range created with Zimbra', ['calendar_range_id' => $calendarRange->getId(), 'calItemId' => $calItemId]);
|
||||
|
||||
$calendarRange->setRemoteId($calItemId);
|
||||
return;
|
||||
} else {
|
||||
($this->updateEvent)($calendarRange);
|
||||
$this->logger->info(self::LOG_PREFIX.'Calendar range updated against zimbra', ['old_cal_remote_id' => $calendarRange->getRemoteId(), 'calendar_range_id' => $calendarRange->getId()]);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Update of calendar: to be implemented');
|
||||
}
|
||||
|
||||
public function syncInvite(Invite $invite): void
|
||||
|
||||
@@ -16,17 +16,35 @@ use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\ZimbraBundle\Exception\CalendarWithoutMainUserException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Zimbra\Mail\Struct\InvitationInfo;
|
||||
use Zimbra\Mail\Struct\InviteComponent;
|
||||
use Zimbra\Mail\Struct\MimePartInfo;
|
||||
use Zimbra\Mail\Struct\Msg;
|
||||
|
||||
/**
|
||||
* Creates a new calendar event in Zimbra.
|
||||
*
|
||||
* This class handles the creation of new calendar events in the Zimbra system.
|
||||
* It uses the Zimbra SOAP API to create events and returns a serialized ID that
|
||||
* can be used as remoteId for Calendar and CalendarRange.
|
||||
*/
|
||||
final readonly class CreateEvent
|
||||
{
|
||||
public function __construct(
|
||||
private SoapClientBuilder $soapClientBuilder,
|
||||
private CreateZimbraComponent $createEvent,
|
||||
private TranslatorInterface $translator,
|
||||
private ZimbraIdSerializer $zimbraIdSerializer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new calendar event in Zimbra.
|
||||
*
|
||||
* @param Calendar|CalendarRange $calendar The calendar event to create
|
||||
*
|
||||
* @return string The serialized Zimbra ID for the created event
|
||||
*
|
||||
* @throws CalendarWithoutMainUserException When the calendar has no associated user email
|
||||
*/
|
||||
public function __invoke(Calendar|CalendarRange $calendar): string
|
||||
{
|
||||
if ($calendar instanceof Calendar) {
|
||||
@@ -59,6 +77,21 @@ final readonly class CreateEvent
|
||||
|
||||
$response = $api->createAppointment($msg, echo: true);
|
||||
|
||||
return $response->getCalItemId();
|
||||
$echo = $response->getEcho();
|
||||
$invite = $echo->getInvite();
|
||||
$MPInviteInfo = $invite->getInvite();
|
||||
/** @var InviteComponent $firstInvite */
|
||||
$firstInvite = $MPInviteInfo->getInviteComponents()[0];
|
||||
|
||||
$id = $this->zimbraIdSerializer->serializeId(
|
||||
$response->getCalItemId(),
|
||||
$response->getCalInvId(),
|
||||
$firstInvite->getUid(),
|
||||
);
|
||||
|
||||
var_dump($response);
|
||||
var_dump($id);
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\ZimbraBundle\Exception\CalendarWithoutMainUserException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Zimbra\Mail\Struct\InvitationInfo;
|
||||
use Zimbra\Mail\Struct\MimePartInfo;
|
||||
use Zimbra\Mail\Struct\Msg;
|
||||
|
||||
/**
|
||||
* Updates an existing calendar event in Zimbra.
|
||||
*
|
||||
* This class handles the modification of existing calendar events in the Zimbra system.
|
||||
* It uses the Zimbra SOAP API to update event details while maintaining the original
|
||||
* event's metadata like IDs and sequences.
|
||||
*/
|
||||
final readonly class UpdateEvent
|
||||
{
|
||||
public function __construct(
|
||||
private SoapClientBuilder $soapClientBuilder,
|
||||
private CreateZimbraComponent $createZimbraComponent,
|
||||
private TranslatorInterface $translator,
|
||||
private ZimbraIdSerializer $zimbraIdSerializer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates an existing calendar event in Zimbra.
|
||||
*
|
||||
* @param Calendar|CalendarRange $calendar The calendar event to update
|
||||
*
|
||||
* @throws CalendarWithoutMainUserException When the calendar has no associated user email
|
||||
*/
|
||||
public function __invoke(Calendar|CalendarRange $calendar): void
|
||||
{
|
||||
if ($calendar instanceof Calendar) {
|
||||
$organizerEmail = $calendar->getMainUser()->getEmail();
|
||||
$organizerLang = $calendar->getMainUser()->getLocale();
|
||||
} else {
|
||||
$organizerEmail = $calendar->getUser()->getEmail();
|
||||
$organizerLang = $calendar->getUser()->getLocale();
|
||||
}
|
||||
|
||||
if (null === $organizerEmail) {
|
||||
throw new CalendarWithoutMainUserException();
|
||||
}
|
||||
|
||||
$api = $this->soapClientBuilder->getApiForAccount($organizerEmail);
|
||||
|
||||
['calItemId' => $calItemId, 'calInvId' => $calInvId, 'inviteComponentCommonUid' => $inviteComponentCommonUid]
|
||||
= $this->zimbraIdSerializer->deSerializeId($calendar->getRemoteId());
|
||||
|
||||
$existing = $api->getAppointment(sync: true, includeContent: true, includeInvites: true, id: $calItemId);
|
||||
$appt = $existing->getApptItem();
|
||||
|
||||
$comp = $this->createZimbraComponent->createZimbraInviteComponentFromCalendar($calendar);
|
||||
$comp->setUid($inviteComponentCommonUid);
|
||||
|
||||
$inv = new InvitationInfo();
|
||||
$inv->setInviteComponent($comp)
|
||||
->setUid($calInvId);
|
||||
|
||||
$mp = new MimePartInfo();
|
||||
$mp->addMimePart(new MimePartInfo('text/plain', $this->translator->trans('zimbra.event_created_by_chill', locale: $organizerLang)));
|
||||
|
||||
$msg = new Msg();
|
||||
$msg->setSubject($this->translator->trans('zimbra.event_created_trough_soap', locale: $organizerLang))
|
||||
->setFolderId('10')
|
||||
->setInvite($inv)
|
||||
->setMimePart($mp)
|
||||
;
|
||||
|
||||
$response = $api->modifyAppointment(
|
||||
id: $calInvId,
|
||||
componentNum: 0,
|
||||
modifiedSequence: $appt->getModifiedSequence(),
|
||||
revision: $appt->getRevision(),
|
||||
msg: $msg,
|
||||
echo: true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
|
||||
|
||||
use Chill\ZimbraBundle\Exception\ZimbraCalendarIdNotDeserializedException;
|
||||
|
||||
/**
|
||||
* Serializes and deserializes Zimbra calendar event IDs.
|
||||
*
|
||||
* This class handles the conversion between Zimbra's individual ID components
|
||||
* and a single serialized string format, allowing for consistent storage and retrieval
|
||||
* of Zimbra calendar event identifiers.
|
||||
*/
|
||||
final readonly class ZimbraIdSerializer
|
||||
{
|
||||
/**
|
||||
* Serializes individual Zimbra calendar ID components into a single string.
|
||||
*
|
||||
* @param string $calItemId The calendar item ID from Zimbra
|
||||
* @param string $calInvId The calendar invitation ID from Zimbra
|
||||
* @param string $inviteComponentCommonUid The common UID for the invite component
|
||||
*
|
||||
* @return string The serialized ID in format "calItemId|calInvId|inviteComponentCommonUid|v0"
|
||||
*/
|
||||
public function serializeId(string $calItemId, string $calInvId, string $inviteComponentCommonUid): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s|%s|%s|v0',
|
||||
$calItemId,
|
||||
$calInvId,
|
||||
$inviteComponentCommonUid,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a Zimbra calendar ID string into its component parts.
|
||||
*
|
||||
* @param string $remoteId The serialized ID, as stored in the remoteId's Calendar or CalendarRange
|
||||
*
|
||||
* @return array{calItemId: string, calInvId: string, inviteComponentCommonUid: string} Associative array containing the ID components
|
||||
*
|
||||
* @throws ZimbraCalendarIdNotDeserializedException If the remote ID format is invalid or incompatible
|
||||
*/
|
||||
public function deSerializeId(string $remoteId): array
|
||||
{
|
||||
if (!str_ends_with($remoteId, 'v0')) {
|
||||
throw new ZimbraCalendarIdNotDeserializedException();
|
||||
}
|
||||
|
||||
$exploded = explode('|', $remoteId);
|
||||
|
||||
return [
|
||||
'calItemId' => $exploded[0],
|
||||
'calInvId' => $exploded[1],
|
||||
'inviteComponentCommonUid' => $exploded[2],
|
||||
];
|
||||
}
|
||||
}
|
||||
178
packages/ChillZimbraBundle/src/Command/TestCommand.php
Normal file
178
packages/ChillZimbraBundle/src/Command/TestCommand.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ZimbraBundle\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\HttpClient\Psr18Client;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Zimbra\Common\Enum\AccountBy;
|
||||
use Zimbra\Common\Enum\FreeBusyStatus;
|
||||
use Zimbra\Common\Enum\InviteClass;
|
||||
use Zimbra\Common\Enum\InviteStatus;
|
||||
use Zimbra\Common\Enum\ItemType;
|
||||
use Zimbra\Common\Enum\Transparency;
|
||||
use Zimbra\Common\Soap\ClientFactory;
|
||||
use Zimbra\Common\Struct\Header\AccountInfo;
|
||||
use Zimbra\Mail\MailApi;
|
||||
use Zimbra\Mail\Struct\DtTimeInfo;
|
||||
use Zimbra\Mail\Struct\Folder;
|
||||
use Zimbra\Mail\Struct\GetFolderSpec;
|
||||
use Zimbra\Mail\Struct\InvitationInfo;
|
||||
use Zimbra\Mail\Struct\InviteComponent;
|
||||
use Zimbra\Mail\Struct\MimePartInfo;
|
||||
use Zimbra\Mail\Struct\Msg;
|
||||
|
||||
class TestCommand extends Command
|
||||
{
|
||||
public function __construct(private HttpClientInterface $client)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'chill:zimbra:test';
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
|
||||
$this->getAppointments($this->createApiForJulien());
|
||||
// $this->createAppointment($this->createApiForJulien());
|
||||
|
||||
// $api = $this->createApiForJulienDelegated();
|
||||
// $this->createAppointment($api);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
public function createApiForJulienDelegated(): MailApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = 'https://zimbra.cldev.ours/service/soap',
|
||||
'verify_host' => false,
|
||||
'verify_peer' => false,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new MailApi();
|
||||
$client = ClientFactory::create($location, $psr18Client);
|
||||
$api->setClient($client);
|
||||
$response = $api->authByAccountName('chill@zimbra.cldev.ours', 'password');
|
||||
|
||||
$token = $response->getAuthToken();
|
||||
|
||||
var_dump($token);
|
||||
|
||||
$apiBy = new MailApi();
|
||||
$apiBy->setClient($client);
|
||||
$apiBy->setAuthToken($token);
|
||||
$apiBy->setTargetAccount(new AccountInfo(AccountBy::NAME, 'julien@zimbra.cldev.ours'));
|
||||
|
||||
return $apiBy;
|
||||
}
|
||||
|
||||
private function createApiForJulien(): MailApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = 'https://zimbra.cldev.ours/service/soap',
|
||||
'verify_host' => false,
|
||||
'verify_peer' => false,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new MailApi();
|
||||
$client = ClientFactory::create($location, $psr18Client);
|
||||
$api->setClient($client);
|
||||
$api->authByAccountName('julien@zimbra.cldev.ours', 'Password;1234');
|
||||
|
||||
return $api;
|
||||
}
|
||||
|
||||
private function updateAppointment(MailApi $api): void
|
||||
{
|
||||
$appointment = $api->getAppointment(id: '69ec3b5e-9f83-4467-a151-a99bc64cfb38:376');
|
||||
|
||||
}
|
||||
|
||||
private function createAppointment(MailApi $api): void
|
||||
{
|
||||
$date = new \DateTimeImmutable('2025-12-03T18:00:00Z');
|
||||
$comp = new InviteComponent();
|
||||
$comp->setName('Test Appointment by chill')
|
||||
->setLocation('Test Location')
|
||||
->setFreeBusy(FreeBusyStatus::BUSY)
|
||||
->setStatus(InviteStatus::CONFIRMED)
|
||||
->setCalClass(InviteClass::PUB)
|
||||
->setTransparency(Transparency::OPAQUE)
|
||||
->setIsAllDay(false)
|
||||
->setIsDraft(false)
|
||||
->setDtStart(new DtTimeInfo($date->format('Ymd\THis'), 'Europe/Brussels'))
|
||||
->setDtEnd(new DtTimeInfo($date->add(new \DateInterval('PT1H'))->format('Ymd\THis'), 'Europe/Brussels'));
|
||||
|
||||
$inv = new InvitationInfo();
|
||||
$inv->setInviteComponent($comp);
|
||||
|
||||
$mp = new MimePartInfo();
|
||||
$mp->addMimePart(new MimePartInfo('text/plain', 'Appointment create via soap'));
|
||||
|
||||
$msg = new Msg();
|
||||
$msg->setSubject('Réunion créée via soap')
|
||||
->setFolderId('10')
|
||||
->setInvite($inv)
|
||||
->setMimePart($mp);
|
||||
|
||||
$response = $api->createAppointment($msg, echo: true);
|
||||
var_dump($response->getEcho());
|
||||
}
|
||||
|
||||
private function getAppointments(MailApi $api): void
|
||||
{
|
||||
|
||||
// $folders = $api->getFolder(new GetFolderSpec(folderId: 10));
|
||||
$foldersResponse = $api->getFolder(viewConstraint: ItemType::APPOINTMENT->value);
|
||||
$calendarFolders = [];
|
||||
|
||||
foreach ($foldersResponse->getFolder()->getSubfolders() as $folder) {
|
||||
if ($folder instanceof Folder) {
|
||||
var_dump($folder->getView()?->value);
|
||||
if ($folder->getView()?->value === ItemType::APPOINTMENT->value) {
|
||||
var_dump('found a calendar');
|
||||
$calendarFolders[] = $folder;
|
||||
}
|
||||
} else {
|
||||
var_dump('not a calendar:'.$folder->getView());
|
||||
}
|
||||
}
|
||||
|
||||
$since = new \DateTimeImmutable('2025-11-28');
|
||||
$until = new \DateTimeImmutable('2025-11-29');
|
||||
|
||||
$allAppointments = [];
|
||||
|
||||
foreach ($calendarFolders as $calendarFolder) {
|
||||
$appointmentIdsInRange = $api
|
||||
->getAppointmentIdsInRange($since->getTimestamp() * 1000, $until->getTimestamp() * 1000, $calendarFolder->getId())
|
||||
->getAppointmentData()
|
||||
;
|
||||
$allAppointments = [...$allAppointments, ...$appointmentIdsInRange];
|
||||
}
|
||||
|
||||
var_dump($allAppointments);
|
||||
|
||||
foreach ($allAppointments as $appointment) {
|
||||
var_dump($appointment->getId());
|
||||
$fetched = $api->getAppointment(id: $appointment->getId());
|
||||
var_dump($fetched);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ZimbraBundle\Exception;
|
||||
|
||||
class ZimbraCalendarIdNotDeserializedException extends \RuntimeException {}
|
||||
Reference in New Issue
Block a user