Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions Pages/Export/CalendarExportDisplay.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use LibreBooking\Calendar\IcsMethod;
use Sabre\VObject\Component\VAlarm;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
Expand All @@ -18,14 +19,16 @@ public function __construct()
/**
* @param iCalendarReservationView[] $reservations
* @param string|null $calendarName Optional display name rendered as X-WR-CALNAME
* @param IcsMethod $method PUBLISH for feeds and exports. Emails send REQUEST, or CANCEL for deletions,
* with the default address as organizer and the recipient as the only attendee.
*/
public function Render(array $reservations, ?string $calendarName = null): string
public function Render(array $reservations, ?string $calendarName = null, IcsMethod $method = IcsMethod::PUBLISH): string
{
// Values passed as constructor children are merged over getDefaults(),
// replacing the PRODID that VCalendar would otherwise generate.
$vcal = new VCalendar([
'PRODID' => '-//LibreBooking//NONSGML ' . Configuration::VERSION . '//EN',
'METHOD' => 'REQUEST',
'METHOD' => $method->value,
]);

if ($calendarName !== null && $calendarName !== '') {
Expand Down Expand Up @@ -54,10 +57,18 @@ public function Render(array $reservations, ?string $calendarName = null): strin
$event->add('DTEND', $res->DateEnd->Format($isoFormat));
$event->add('LAST-MODIFIED', $res->LastModified->Format($isoFormat));
$event->add('LOCATION', $res->Location);
$event->add('ORGANIZER', 'mailto:' . $res->OrganizerEmail, ['CN' => $res->Organizer]);
$event->add('STATUS', $res->IsPending ? 'TENTATIVE' : 'CONFIRMED');
if ($method !== IcsMethod::PUBLISH && !empty($res->OrganizerSentBy)) {
$event->add('ORGANIZER', 'mailto:' . $res->OrganizerSentBy, ['CN' => $res->SenderName]);
} elseif (!empty($res->OrganizerEmail)) {
$organizerParams = ['CN' => $res->Organizer];
if (!empty($res->OrganizerSentBy)) {
// RFC 5545 §3.2.18: the site's address sends on the owner's behalf
$organizerParams['SENT-BY'] = 'mailto:' . $res->OrganizerSentBy;
}
$event->add('ORGANIZER', 'mailto:' . $res->OrganizerEmail, $organizerParams);
}
$event->add('STATUS', $method === IcsMethod::CANCEL ? 'CANCELLED' : ($res->IsPending ? 'TENTATIVE' : 'CONFIRMED'));
$event->add('SUMMARY', $res->Summary);
$event->add('SEQUENCE', 0);
$event->add('URL', $res->ReservationUrl);
$event->add('X-MICROSOFT-CDO-BUSYSTATUS', 'BUSY');

Expand Down Expand Up @@ -88,6 +99,17 @@ public function Render(array $reservations, ?string $calendarName = null): strin
}
}

$attendeeEmail = $res->AttendeeEmail ?? $res->OrganizerEmail;
if ($method !== IcsMethod::PUBLISH && !empty($res->OrganizerSentBy) && !empty($attendeeEmail)) {
$attendeeName = $res->AttendeeEmail ? $res->AttendeeName : $res->Organizer;
$event->add('ATTENDEE', 'mailto:' . $attendeeEmail, array_filter([
'CN' => $attendeeName,
'ROLE' => 'REQ-PARTICIPANT',
'PARTSTAT' => 'NEEDS-ACTION',
'RSVP' => 'FALSE',
]));
}

if ($res->StartReminder !== null) {
$alarm = new VAlarm($vcal, 'VALARM');
$event->add($alarm);
Expand Down
34 changes: 21 additions & 13 deletions lib/Application/Schedule/iCalendarReservationView.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class iCalendarReservationView
public $Description;
public $Organizer;
public $OrganizerEmail;
public $OrganizerSentBy;
public $SenderName;
public $AttendeeName;
public $AttendeeEmail;
public $RecurRule;
public $ReferenceNumber;
public $Summary;
Expand Down Expand Up @@ -60,12 +64,6 @@ public function __construct($res, UserSession $currentUser, IPrivacyFilter $priv
$this->ExportFactory = PluginManager::Instance()->LoadExport();

$privateNotice = 'Private';
$privateEmail = Configuration::Instance()->GetKey(ConfigKeys::EMAIL_DEFAULT_FROM_ADDRESS);
static $missingPrivateOrganizerEmailLogged = false;
if (!$canViewUser && $privateEmail === '' && !$missingPrivateOrganizerEmailLogged) {
Log::Error('ICS private organizer email is not configured. Set the email.default.from.address setting.');
$missingPrivateOrganizerEmailLogged = true;
}

$this->Classification = method_exists($this->ExportFactory, 'GetIcalendarClassification') ? $this->ExportFactory->GetIcalendarClassification($res) : 'PUBLIC';
if ($res->DateCreated) {
Expand All @@ -78,9 +76,23 @@ public function __construct($res, UserSession $currentUser, IPrivacyFilter $priv
$this->DateStart = $res->StartDate;
$this->Summary = $canViewDetails ? $factory->Format($res, $summaryFormat) : $privateNotice;
$this->Description = $canViewDetails ? ($res->Description ?? '') : $privateNotice;
$fullName = new FullName($res->OwnerFirstName, $res->OwnerLastName);
$this->Organizer = $canViewUser ? $fullName->__toString() : $privateNotice;
$this->OrganizerEmail = $canViewUser ? $res->OwnerEmailAddress : $privateEmail;

$defaultFromAddress = Configuration::Instance()->GetKey(ConfigKeys::EMAIL_DEFAULT_FROM_ADDRESS);
if ($canViewUser) {
$ownerName = new FullName($res->OwnerFirstName, $res->OwnerLastName);
$this->Organizer = $ownerName->__toString();
$this->OrganizerEmail = $res->OwnerEmailAddress;
$this->OrganizerSentBy = $defaultFromAddress;
$this->SenderName = Configuration::Instance()->GetKey(ConfigKeys::EMAIL_DEFAULT_FROM_NAME);
} else {
$this->Organizer = $privateNotice;
$this->OrganizerEmail = $defaultFromAddress;
static $missingPrivateOrganizerEmailLogged = false;
if ($defaultFromAddress === '' && !$missingPrivateOrganizerEmailLogged) {
Log::Error('ICS private organizer email is not configured. Set the email.default.from.address setting.');
$missingPrivateOrganizerEmailLogged = true;
}
}
$this->RecurRule = $this->CreateRecurRule($res);
$this->ReferenceNumber = $res->ReferenceNumber;
$this->ReservationUrl = sprintf(
Expand All @@ -97,10 +109,6 @@ public function __construct($res, UserSession $currentUser, IPrivacyFilter $priv
$this->LastModified = empty($res->ModifiedDate) || $res->ModifiedDate->ToString() == '' ? $this->DateCreated : $res->ModifiedDate;
$this->IsPending = $res->RequiresApproval;

if ($canViewUser && $res->OwnerId == $currentUser->UserId) {
$this->OrganizerEmail = str_replace('@', '-noreply@', $res->OwnerEmailAddress);
}

$this->ExtraIcalLines = method_exists($this->ExportFactory, 'GetIcalendarExtraLines') ? $this->ExportFactory->GetIcalendarExtraLines($res) : null;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Email/IEmailMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function ReplyTo();
* @abstract
* @param string $contents
* @param string $fileName
* @param string|null $mimeType Full Content-Type value (e.g. 'text/calendar; method=REQUEST'). Null auto-detects from $fileName.
* @param string|null $mimeType Full Content-Type value (e.g. 'text/calendar; method=PUBLISH'). Null auto-detects from $fileName.
*/
public function AddStringAttachment($contents, $fileName, ?string $mimeType = null);

Expand Down
7 changes: 7 additions & 0 deletions lib/Email/Messages/ReservationDeletedEmail.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use LibreBooking\Calendar\IcsMethod;

require_once(ROOT_DIR . 'lib/Email/Messages/ReservationEmailMessage.php');

class ReservationDeletedEmail extends ReservationEmailMessage
Expand All @@ -25,4 +27,9 @@ protected function GetTemplateName()
{
return 'ReservationDeleted.tpl';
}

protected function GetIcsMethod(): IcsMethod
{
return IcsMethod::CANCEL;
}
}
27 changes: 24 additions & 3 deletions lib/Email/Messages/ReservationEmailMessage.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use LibreBooking\Calendar\IcsMethod;

require_once(ROOT_DIR . 'lib/Email/namespace.php');
require_once(ROOT_DIR . 'Pages/Pages.php');
require_once(ROOT_DIR . 'Pages/Export/CalendarExportDisplay.php');
Expand Down Expand Up @@ -215,14 +217,33 @@ protected function PopulateIcsAttachment($currentInstance, $attributeValues)
$rv->UserPreferences = $this->reservationOwner->GetPreferences();
$rv->OwnerEmailAddress = $this->reservationOwner->EmailAddress();

$icsView = new iCalendarReservationView($rv, $this->reservationSeries->BookedBy(), new NullPrivacyFilter());
// BookedBy() is null for series from ReservationRepository::BuildSeries(); use the
// owner, with a timezone for the {startdate}/{enddate} summary placeholders.
$currentUser = $this->reservationSeries->BookedBy();
if ($currentUser === null) {
$currentUser = new UserSession($this->reservationOwner->Id());
$currentUser->Timezone = $this->timezone;
}
$icsView = new iCalendarReservationView($rv, $currentUser, new NullPrivacyFilter());
// the attendee is whoever the email goes to: the owner, or an invitee, guest or share recipient
$recipient = $this->To();
$recipient = is_array($recipient) ? $recipient[0] : $recipient;
$icsView->AttendeeName = $recipient->Name();
$icsView->AttendeeEmail = $recipient->Address();

$method = $this->GetIcsMethod();

$display = new CalendarExportDisplay();
$icsContents = $display->Render([$icsView]);
$icsContents = $display->Render([$icsView], null, $method);
$this->AddStringAttachment(
contents: $icsContents,
fileName: 'reservation.ics',
mimeType: 'text/calendar; charset=UTF-8; method=REQUEST'
mimeType: "text/calendar; charset=UTF-8; method={$method->value}"
);
}

protected function GetIcsMethod(): IcsMethod
{
return IcsMethod::REQUEST;
}
}
2 changes: 1 addition & 1 deletion lib/Email/Messages/ReservationUpdatedEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ReservationUpdatedEmail extends ReservationEmailMessage
{
public function Subject()
{
return $this->Translate('ReservationUpdatedAdminSubjectWithResource', [$this->primaryResource->GetName()]);
return $this->Translate('ReservationUpdatedSubjectWithResource', [$this->primaryResource->GetName()]);
}

protected function GetTemplateName()
Expand Down
15 changes: 15 additions & 0 deletions src/Calendar/IcsMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace LibreBooking\Calendar;

/**
* RFC 5546 iTip method for an ICS VCALENDAR.
*/
enum IcsMethod: string
{
case PUBLISH = 'PUBLISH';
case REQUEST = 'REQUEST';
case CANCEL = 'CANCEL';
}
Loading
Loading