/home/ivoiecob/email.hirewise-va.com/modules/CalendarMeetingsPlugin/Manager.php
<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace Aurora\Modules\CalendarMeetingsPlugin;

use Aurora\Modules\Core\Models\User;
use Aurora\Modules\Mail\Models\MailAccount;
use Aurora\System\Api;
use Aurora\Modules\Core\Module as CoreModule;
use Aurora\System\Enums\LogLevel;

/**
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2023, Afterlogic Corp.
 */
class Manager extends \Aurora\Modules\Calendar\Manager
{
    /**
     * Processing response to event invitation. [Aurora only.](http://dev.afterlogic.com/aurora)
     *
     * @param string $sUserPublicId
     * @param string $sCalendarId Calendar ID
     * @param string $sEventId Event ID
     * @param string $sAttendee Attendee identified by email address
     * @param string $sAction Appointment actions. Accepted values:
     *		- "ACCEPTED"
     *		- "DECLINED"
     *		- "TENTATIVE"
     *
     * @return bool
     */
    public function updateAppointment($sUserPublicId, $sCalendarId, $sEventId, $sAttendee, $sAction)
    {
        $oResult = null;

        $aData = $this->oStorage->getEvent($sUserPublicId, $sCalendarId, $sEventId);
        if ($aData !== false) {
            $oVCal = $aData['vcal'];
            $oVCal->METHOD = 'REQUEST';
            return $this->appointmentAction($sUserPublicId, $sAttendee, $sAction, $sCalendarId, $oVCal->serialize());
        }

        return $oResult;
    }

    /**
     * @param string $sPartstat
     * @param string $sSummary
     */
    protected function getMessageSubjectFromPartstat($sPartstat, $sSummary)
    {
        $sSubject = $sSummary;
        switch ($sPartstat) {
            case 'ACCEPTED':
                $sSubject = $this->GetModule()->i18N('SUBJECT_PREFFIX_ACCEPTED') . ': ' . $sSummary;
                break;
            case 'DECLINED':
                $sSubject = $this->GetModule()->i18N('SUBJECT_PREFFIX_DECLINED') . ': ' . $sSummary;
                break;
            case 'TENTATIVE':
                $sSubject = $this->GetModule()->i18N('SUBJECT_PREFFIX_TENTATIVE') . ': ' . $sSummary;
                break;
        }

        return $sSubject;
    }

    /**
     * @param User $oUser
     * @param string $sAttendee
     */
    protected function getFromAccount($oUser, $sAttendee)
    {
        $oFromAccount = null;

        if ($oUser && $oUser->PublicId !== $sAttendee) {
            $oMailModule = Api::GetModule('Mail');
            /** @var \Aurora\Modules\Mail\Module $oMailModule */
            if ($oMailModule) {
                $aAccounts = $oMailModule->getAccountsManager()->getUserAccounts($oUser->Id);
                foreach ($aAccounts as $oAccount) {
                    if ($oAccount instanceof MailAccount && $oAccount->Email === $sAttendee) {
                        $oFromAccount = $oAccount;
                        break;
                    }
                }
            }
        }

        return $oFromAccount;
    }

    /**
     * Allows for responding to event invitation (accept / decline / tentative). [Aurora only.](http://dev.afterlogic.com/aurora)
     *
     * @param int|string $sUserPublicId Account object
     * @param string $sAttendee Attendee identified by email address
     * @param string $sAction Appointment actions. Accepted values:
     *		- "ACCEPTED"
     *		- "DECLINED"
     *		- "TENTATIVE"
     * @param string $sCalendarId Calendar ID
     * @param string $sData ICS data of the response
     * @param bool $bIsLinkAction If **true**, the attendee's action on the link is assumed
     * @param bool $bIsExternalAttendee
     *
     * @return bool
     */
    public function appointmentAction($sUserPublicId, $sAttendee, $sAction, $sCalendarId, $sData, $AllEvents = 2, $RecurrenceId = null, $bIsLinkAction = false, $bIsExternalAttendee = false)
    {
        $oUser = null;
        $bResult = false;
        $sEventId = null;
        $sTo = $sSubject = '';

        $oUser = CoreModule::Decorator()->GetUserByPublicId($sUserPublicId);

        if (!($oUser instanceof User)) {
            throw new Exceptions\Exception(
                Enums\ErrorCodes::CannotSendAppointmentMessage,
                null,
                'User not found'
            );
        }

        if ($bIsLinkAction && !$bIsExternalAttendee) {
            // getting default calendar for attendee
            $oCalendar = $this->getDefaultCalendar($oUser->PublicId);
            if ($oCalendar) {
                $sCalendarId = $oCalendar->Id;
            }
        }

        /** @var \Sabre\VObject\Component\VCalendar $oVCal */
        $oVCal = \Sabre\VObject\Reader::read($sData);

        if ($oVCal) {
            $sMethod = strtoupper((string) $oVCal->METHOD);
            $sPartstat = strtoupper($sAction);
            $sCN = '';
            if ($sAttendee ===  $oUser->PublicId) {
                $sCN = !empty($oUser->Name) ? $oUser->Name : $sAttendee;
            }
            $bNeedsToUpdateEvent = false;

            // Now we need to loop through the original organizer event, to find
            // all the instances where we have a reply for.
            $masterEvent = $oVCal->getBaseComponent('VEVENT');

            if (!$masterEvent) {
                // No master event, we can't add new instances.
                return false;
            }

            $sEventId = (string) $masterEvent->UID;
            if ($AllEvents === 2) {
                if ($sPartstat === 'DECLINED' || $sMethod === 'CANCEL') {
                    if ($sCalendarId !== false) {
                        $this->deleteEvent($sAttendee, $sCalendarId, $sEventId);
                    }
                } else {
                    $bNeedsToUpdateEvent = true;
                }

                $attendeeFound = false;
                if (isset($masterEvent->ATTENDEE)) {
                    foreach ($masterEvent->ATTENDEE as $attendee) {
                        $sEmail = str_replace('mailto:', '', strtolower($attendee->getValue()));
                        if (strtolower($sEmail) === strtolower($sAttendee)) {
                            $attendeeFound = true;
                            $attendee['PARTSTAT'] = $sPartstat;
                            $attendee['RESPONDED-AT'] = gmdate("Ymd\THis\Z");
                            // Un-setting the RSVP status, because we now know
                            // that the attendee already replied.
                            unset($attendee['RSVP']);
                            break;
                        }
                    }
                }

                if (!$attendeeFound) {
                    // Adding a new attendee. The iTip documentation calls this
                    // a party crasher.
                    $attendee = $masterEvent->add('ATTENDEE', $sAttendee, [
                        'PARTSTAT' => $sPartstat,
                        'CN' => $sCN,
                        'RESPONDED-AT' => gmdate("Ymd\THis\Z")
                    ]);
                }
            }

            $masterEvent->{'LAST-MODIFIED'} = new \DateTime('now', new \DateTimeZone('UTC'));

            if ($AllEvents === 1 && $RecurrenceId !== null) {
                if ($sPartstat === 'DECLINED' || $sMethod === 'CANCEL') {
                    if ($sCalendarId !== false) {
                        $oEvent = new \Aurora\Modules\Calendar\Classes\Event();
                        $oEvent->IdCalendar = $sCalendarId;
                        $oEvent->Id = $sEventId;
                        $this->updateExclusion($sAttendee, $oEvent, $RecurrenceId, true);
                    }
                } else {
                    $bNeedsToUpdateEvent = true;
                }

                $vevent = null;
                $index = \Aurora\Modules\Calendar\Classes\Helper::isRecurrenceExists($oVCal->VEVENT, $RecurrenceId);
                if ($index === false) {
                    // If we got replies to instances that did not exist in the
                    // original list, it means that new exceptions must be created.
                    $recurrenceIterator = new \Sabre\VObject\Recur\EventIterator($oVCal, $masterEvent->UID);
                    $found = false;
                    $iterations = 1000;
                    do {
                        $newObject = $recurrenceIterator->getEventObject();
                        $recurrenceIterator->next();

                        if (isset($newObject->{'RECURRENCE-ID'})) {
                            $iRecurrenceId = \Aurora\Modules\Calendar\Classes\Helper::getTimestamp($newObject->{'RECURRENCE-ID'}, $oUser->DefaultTimeZone);
                            if ((int) $iRecurrenceId === (int) $RecurrenceId) {
                                $found = true;
                            }
                        }
                        --$iterations;
                    } while ($recurrenceIterator->valid() && !$found && $iterations);

                    if ($found) {
                        unset(
                            $newObject->RRULE,
                            $newObject->EXDATE,
                            $newObject->RDATE
                        );
                        $vevent = $oVCal->add($newObject);
                    }
                } else {
                    $vevent = $oVCal->VEVENT[$index];
                }

                $attendeeFound = false;
                if (isset($vevent->ATTENDEE)) {
                    foreach ($vevent->ATTENDEE as $attendee) {
                        $sEmail = str_replace('mailto:', '', strtolower($attendee->getValue()));
                        if (strtolower($sEmail) === strtolower($sAttendee)) {
                            $attendeeFound = true;
                            $attendee['PARTSTAT'] = $sPartstat;
                            $attendee['RESPONDED-AT'] = gmdate("Ymd\THis\Z");
                            break;
                        }
                    }
                }
                if ($vevent && !$attendeeFound) {
                    // Adding a new attendee
                    $attendee = $vevent->add('ATTENDEE', $sAttendee, [
                        'PARTSTAT' => $sPartstat,
                        'CN' => $sCN,
                        'RESPONDED-AT' => gmdate("Ymd\THis\Z")
                    ]);
                }
            }

            if ($sMethod === 'REQUEST') {
                $sMethod = 'REPLY';
            }

            $oVCalForSend = clone $oVCal;
            $oVCalForSend->METHOD = $sMethod;

            $sTo = isset($masterEvent->ORGANIZER) ? str_replace(['mailto:', 'principals/'], '', strtolower((string) $masterEvent->ORGANIZER)) : '';
            $sSummary = isset($masterEvent->SUMMARY) ? (string) $masterEvent->SUMMARY : '';
            $sSubject = $this->getMessageSubjectFromPartstat($sPartstat, $sSummary);

            if ($bNeedsToUpdateEvent) { // update event on server
                unset($oVCal->METHOD);
                $this->oStorage->updateEventRaw(
                    $oUser->PublicId,
                    $sCalendarId,
                    $sEventId,
                    $oVCal->serialize()
                );
            }

            if ($oVCalForSend) { //send message to organizer
                if (empty($sTo)) {
                    throw new Exceptions\Exception(
                        Enums\ErrorCodes::CannotSendAppointmentMessageNoOrganizer
                    );
                }
                $oFromAccount = $this->getFromAccount($oUser, $sAttendee);
                $bResult = Classes\Helper::sendAppointmentMessage(
                    $oUser->PublicId,
                    $sTo,
                    $sSubject,
                    $oVCalForSend,
                    $sMethod,
                    '',
                    $oFromAccount,
                    $sAttendee
                );
            }
        }

        if (!$bResult) {
            Api::Log('Ics Appointment Action FALSE result!', LogLevel::Error);
            if ($sUserPublicId) {
                Api::Log('Email: ' . $oUser->PublicId . ', Action: ' . $sAction . ', Data:', LogLevel::Error);
            }
            Api::Log($sData, LogLevel::Error);
        } else {
            $bResult = $sEventId;
        }

        return $bResult;
    }
}