1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\Calendar;
9:
10: require_once \dirname(__file__) . "/../../system/autoload.php";
11: \Aurora\System\Api::Init();
12:
13: class Reminder
14: {
15: private $oUsersManager;
16:
17: private $oCalendarManager;
18:
19: private $oMailManager;
20:
21: private $oAccountsManager;
22:
23: private $oCalendarModule;
24:
25: /**
26: * @var array
27: */
28: private $aUsers;
29:
30: /**
31: * @var string
32: */
33: private $sCurRunFilePath;
34:
35: public function __construct()
36: {
37: $this->aUsers = array();
38: $this->sCurRunFilePath = \Aurora\System\Api::DataPath() . '/reminder-run';
39:
40: $oMailModule = \Aurora\Modules\Mail\Module::getInstance();
41: $this->oCalendarModule = \Aurora\Modules\Calendar\Module::getInstance();
42:
43: $this->oUsersManager = \Aurora\Modules\Core\Module::getInstance()->getUsersManager() ;
44: $this->oCalendarManager = $this->oCalendarModule->getManager();
45: $this->oMailManager = $oMailModule->getMailManager();
46: $this->oAccountsManager = $oMailModule->getAccountsManager();
47: }
48:
49: public static function NewInstance()
50: {
51: return new self();
52: }
53:
54: /**
55: * @param string $sKey
56: * @param \Aurora\Modules\Core\Models\User $oUser = null
57: * @param array $aParams = null
58: * @param int $iMinutes
59: *
60: * @return string
61: */
62: private function i18n($sKey, $oUser = null, $aParams = null, $iMinutes = null)
63: {
64: return $this->oCalendarModule->I18N($sKey, $aParams, $iMinutes, $oUser->UUID);
65: }
66:
67: /**
68: * @param string $sLogin
69: *
70: * @return \Aurora\Modules\Core\Models\User
71: */
72: private function &getUser($sLogin)
73: {
74: $mResult = null;
75:
76: if (!isset($this->aUsers[$sLogin])) {
77: $this->aUsers[$sLogin] = $this->oUsersManager->getUserByPublicId($sLogin);
78: }
79:
80: $mResult = &$this->aUsers[$sLogin];
81:
82: if (is_array($this->aUsers[$sLogin]) && 30 < count($this->aUsers[$sLogin])) {
83: $this->aUsers = array_slice($this->aUsers, -30);
84: }
85:
86: return $mResult;
87: }
88:
89: /**
90: * @param \Aurora\Modules\Core\Models\User $oUser
91: * @param string $sEventName
92: * @param string $sDateStr
93: * @param string $sCalendarName
94: * @param string $sEventText
95: * @param string $sCalendarColor
96: *
97: * @return string
98: */
99: private function createBodyHtml($oUser, $sEventName, $sDateStr, $sCalendarName, $sEventText, $sCalendarColor)
100: {
101: $sEventText = nl2br($sEventText);
102:
103: return sprintf(
104: '
105: <div style="padding: 10px; font-size: 12px; text-align: center; word-wrap: break-word;">
106: <div style="border: 4px solid %s; padding: 15px; width: 370px;">
107: <h2 style="margin: 5px; font-size: 18px; line-height: 1.4;">%s</h2>
108: <span>%s%s</span><br/>
109: <span>%s: %s</span><br/><br/>
110: <span>%s</span><br/>
111: </div>
112: <p style="color:#667766; width: 400px; font-size: 10px;">%s</p>
113: </div>',
114: $sCalendarColor,
115: $sEventName,
116: ucfirst($this->i18n('EVENT_BEGIN', $oUser)),
117: $sDateStr,
118: $this->i18n('CALENDAR', $oUser),
119: $sCalendarName,
120: $sEventText,
121: $this->i18n('EMAIL_EXPLANATION', $oUser, array(
122: 'EMAIL' => '<a href="mailto:' . $oUser->PublicId . '">' . $oUser->PublicId . '</a>',
123: 'CALENDAR_NAME' => $sCalendarName
124: ))
125: );
126: }
127:
128: /**
129: * @param \Aurora\Modules\Core\Models\User $oUser
130: * @param string $sEventName
131: * @param string $sDateStr
132: * @param string $sCalendarName
133: * @param string $sEventText
134: *
135: * @return string
136: */
137: private function createBodyText($oUser, $sEventName, $sDateStr, $sCalendarName, $sEventText)
138: {
139: return sprintf(
140: "%s\r\n\r\n%s%s\r\n\r\n%s: %s %s\r\n\r\n%s",
141: $sEventName,
142: ucfirst($this->i18n('EVENT_BEGIN', $oUser)),
143: $sDateStr,
144: $this->i18n('CALENDAR', $oUser),
145: $sCalendarName,
146: $sEventText,
147: $this->i18n('EMAIL_EXPLANATION', $oUser, array(
148: 'EMAIL' => '<a href="mailto:' . $oUser->PublicId . '">' . $oUser->PublicId . '</a>',
149: 'CALENDAR_NAME' => $sCalendarName
150: ))
151: );
152: }
153:
154: /**
155: * @param \Aurora\Modules\Core\Models\User $oUser
156: * @param string $sSubject
157: * @param string $mHtml = null
158: * @param string $mText = null
159: *
160: * @return \MailSo\Mime\Message
161: */
162: private function createMessage($oUser, $sSubject, $mHtml = null, $mText = null)
163: {
164: $oMessage = \MailSo\Mime\Message::NewInstance();
165: $oMessage->RegenerateMessageId();
166:
167: // $sXMailer = \Aurora\System\Api::GetValue('webmail.xmailer-value', '');
168: // if (0 < strlen($sXMailer))
169: // {
170: // $oMessage->SetXMailer($sXMailer);
171: // }
172:
173: $oMessage
174: ->SetFrom(\MailSo\Mime\Email::NewInstance($oUser->PublicId))
175: ->SetSubject($sSubject)
176: ;
177:
178: $oToEmails = \MailSo\Mime\EmailCollection::NewInstance($oUser->PublicId);
179: if ($oToEmails && $oToEmails->Count()) {
180: $oMessage->SetTo($oToEmails);
181: }
182:
183: if ($mHtml !== null) {
184: $oMessage->AddText($mHtml, true);
185: }
186:
187: if ($mText !== null) {
188: $oMessage->AddText($mText, false);
189: }
190:
191: return $oMessage;
192: }
193:
194: /**
195: *
196: * @param \Aurora\Modules\Core\Models\User $oUser
197: * @param string $sSubject
198: * @param string $sEventName
199: * @param string $sDate
200: * @param string $sCalendarName
201: * @param string $sEventText
202: * @param string $sCalendarColor
203: *
204: * @return bool
205: */
206: private function sendMessage($oUser, $sSubject, $sEventName, $sDate, $sCalendarName, $sEventText, $sCalendarColor)
207: {
208: $oMessage = $this->createMessage(
209: $oUser,
210: $sSubject,
211: $this->createBodyHtml($oUser, $sEventName, $sDate, $sCalendarName, $sEventText, $sCalendarColor),
212: $this->createBodyText($oUser, $sEventName, $sDate, $sCalendarName, $sEventText)
213: );
214:
215: try {
216: $oAccount = $this->oAccountsManager->getAccountUsedToAuthorize($oUser->PublicId);
217: if (!$oAccount instanceof \Aurora\Modules\Mail\Models\MailAccount) {
218: return false;
219: }
220: return $this->oMailManager->sendMessage($oAccount, $oMessage);
221: } catch (\Exception $oException) {
222: \Aurora\System\Api::Log('MessageSend Exception', \Aurora\System\Enums\LogLevel::Error, 'cron-');
223: \Aurora\System\Api::LogException($oException, \Aurora\System\Enums\LogLevel::Error, 'cron-');
224: }
225:
226: return false;
227: }
228:
229: private function getSubject($oUser, $sEventStart, $iEventStartTS, $sEventName, $sDate, $iNowTS, $bAllDay = false)
230: {
231: $sSubject = '';
232:
233: if ($bAllDay) {
234: $oEventStart = new \DateTime("@$iEventStartTS", new \DateTimeZone('UTC'));
235: $oEventStart->setTimezone(new \DateTimeZone($oUser->DefaultTimeZone ?: 'UTC'));
236: $iEventStartTS = $oEventStart->getTimestamp() - $oEventStart->getOffset();
237: }
238:
239: $iMinutes = round(($iEventStartTS - $iNowTS) / 60);
240:
241: if ($iMinutes > 0 && $iMinutes < 60) {
242: $sSubject = $this->i18n('SUBJECT_MINUTES_PLURAL', $oUser, array(
243: 'EVENT_NAME' => $sEventName,
244: 'DATE' => date('G:i', strtotime($sEventStart)),
245: 'COUNT' => $iMinutes
246: ), $iMinutes);
247: } elseif ($iMinutes >= 60 && $iMinutes < 1440) {
248: $sSubject = $this->i18n('SUBJECT_HOURS_PLURAL', $oUser, array(
249: 'EVENT_NAME' => $sEventName,
250: 'DATE' => date('G:i', strtotime($sEventStart)),
251: 'COUNT' => round($iMinutes / 60)
252: ), round($iMinutes / 60));
253: } elseif ($iMinutes >= 1440 && $iMinutes < 10080) {
254: $sSubject = $this->i18n('SUBJECT_DAYS_PLURAL', $oUser, array(
255: 'EVENT_NAME' => $sEventName,
256: 'DATE' => $sDate,
257: 'COUNT' => round($iMinutes / 1440)
258: ), round($iMinutes / 1440));
259: } elseif ($iMinutes >= 10080) {
260: $sSubject = $this->i18n('SUBJECT_WEEKS_PLURAL', $oUser, array(
261: 'EVENT_NAME' => $sEventName,
262: 'DATE' => $sDate,
263: 'COUNT' => round($iMinutes / 10080)
264: ), round($iMinutes / 10080));
265: } else {
266: $sSubject = $this->i18n('SUBJECT', $oUser, array(
267: 'EVENT_NAME' => $sEventName,
268: 'DATE' => $sDate
269: ));
270: }
271:
272: return $sSubject;
273: }
274:
275: private function getDateTimeFormat($oUser)
276: {
277: $sDateFormat = 'm/d/Y';
278: $sTimeFormat = 'h:i A';
279:
280: if ($oUser->DateFormat === \Aurora\System\Enums\DateFormat::DDMMYYYY) {
281: $sDateFormat = 'd/m/Y';
282: } elseif ($oUser->DateFormat === \Aurora\System\Enums\DateFormat::MMDDYYYY) {
283: $sDateFormat = 'm/d/Y';
284: } elseif ($oUser->DateFormat === \Aurora\System\Enums\DateFormat::DD_MONTH_YYYY) {
285: $sDateFormat = 'd m Y';
286: } elseif ($oUser->DateFormat === \Aurora\System\Enums\DateFormat::MMDDYY) {
287: $sDateFormat = 'm/d/y';
288: } elseif ($oUser->DateFormat === \Aurora\System\Enums\DateFormat::DDMMYY) {
289: $sDateFormat = 'd/m/Y';
290: }
291:
292: if ($oUser->TimeFormat == \Aurora\System\Enums\TimeFormat::F24) {
293: $sTimeFormat = 'H:i';
294: }
295:
296: return $sDateFormat . ' ' . $sTimeFormat;
297: }
298:
299: public function GetReminders($iStart, $iEnd)
300: {
301: $aReminders = $this->oCalendarManager->getReminders($iStart, $iEnd);
302: $aEvents = array();
303:
304: if ($aReminders && is_array($aReminders) && count($aReminders) > 0) {
305: foreach ($aReminders as $aReminder) {
306: $oUser = $this->getUser($aReminder['user']);
307:
308: $sCalendarUri = $aReminder['calendaruri'];
309: $sEventId = $aReminder['eventid'];
310: $iStartTime = $aReminder['starttime'];
311: $iReminderTime = $aReminder['time'];
312:
313: $aEventData = [];
314: if ($oUser) {
315: $aEventData['data'] = $this->oCalendarManager->getEvent($oUser->PublicId, $sCalendarUri, $sEventId);
316:
317: $dt = new \DateTime();
318: $dt->setTimestamp($iStartTime);
319: $oDefaultTimeZone = new \DateTimeZone($oUser->DefaultTimeZone ?: 'UTC');
320: $dt->setTimezone($oDefaultTimeZone);
321:
322: if (is_array($aEventData['data'])) {
323: $CurrentEvent = null;
324: foreach ($aEventData['data'] as $key => $aEvent) {
325: if (is_int($key)) {
326: if (empty($CurrentEvent)) {
327: $CurrentEvent = $aEvent;
328: } elseif (isset($aEvent['excluded']) && $this->EventHasReminder($aEvent, $iReminderTime)) {
329: $CurrentEvent = $aEvent;
330: }
331: unset($aEventData['data'][$key]);
332: }
333: }
334: if (!empty($CurrentEvent)) {
335: $aEventData['data'][0] = $CurrentEvent;
336: }
337: }
338: $aEventData['time'] = $dt->format($this->getDateTimeFormat($oUser));
339: }
340:
341: if (count($aEventData) > 0) {
342: $aEvents[$aReminder['user']][$sCalendarUri][$sEventId] = $aEventData;
343: }
344: }
345: }
346:
347: return $aEvents;
348: }
349:
350: public function DeleteOutdatedReminders($iTime)
351: {
352: $this->oCalendarManager->deleteOutdatedReminders($iTime);
353: }
354:
355: public function EventHasReminder($aEvent, $iReminderTime)
356: {
357: foreach ($aEvent['alarms'] as $iAlarm) {
358: if ($aEvent['startTS'] - $iAlarm * 60 === (int) $iReminderTime) {
359: return true;
360: }
361: }
362: return false;
363: }
364:
365: public function Execute()
366: {
367: \Aurora\System\Api::Log('---------- Start cron script', \Aurora\System\Enums\LogLevel::Full, 'cron-');
368:
369: $oTimeZoneUTC = new \DateTimeZone('UTC');
370: $oNowDT_UTC = new \DateTimeImmutable('now', $oTimeZoneUTC);
371: $iNowTS = $oNowDT_UTC->getTimestamp();
372:
373: $oStartDT_UTC = clone $oNowDT_UTC;
374: $oStartDT_UTC = $oStartDT_UTC->sub(new \DateInterval('PT30M'));
375:
376: if (file_exists($this->sCurRunFilePath)) {
377: $handle = fopen($this->sCurRunFilePath, 'r');
378: $sCurRunFileTS = fread($handle, 10);
379: if (!empty($sCurRunFileTS) && is_numeric($sCurRunFileTS)) {
380: $oStartDT_UTC = new \DateTimeImmutable("@$sCurRunFileTS");
381: }
382: }
383:
384: $iStartTS = $oStartDT_UTC->getTimestamp();
385:
386: if ($iNowTS >= $iStartTS) {
387: \Aurora\System\Api::Log('Start time: ' . $oStartDT_UTC->format('r'), \Aurora\System\Enums\LogLevel::Full, 'cron-');
388: \Aurora\System\Api::Log('End time: ' . $oNowDT_UTC->format('r'), \Aurora\System\Enums\LogLevel::Full, 'cron-');
389:
390: $aEvents = $this->GetReminders($iStartTS, $iNowTS);
391:
392: foreach ($aEvents as $sEmail => $aUserCalendars) {
393: foreach ($aUserCalendars as $sCalendarUri => $aUserEvents) {
394: foreach ($aUserEvents as $aUserEvent) {
395: $aSubEvents = $aUserEvent['data'];
396:
397: if (isset($aSubEvents, $aSubEvents['vcal'])) {
398: $vCal = $aSubEvents['vcal'];
399: foreach ($aSubEvents as $mKey => $aEvent) {
400: if ($mKey !== 'vcal') {
401: $oUser = $this->getUser($sEmail);
402: $oCalendar = $this->oCalendarManager->getCalendar($oUser->PublicId, $sCalendarUri);
403:
404: if ($oCalendar && $oCalendar->Id === $aEvent['calendarId']) {
405: $sEventCalendarId = $aEvent['calendarId'];
406: $sEventId = $aEvent['uid'];
407: $sEventStart = $aEvent['start'];
408: $iEventStartTS = $aEvent['startTS'];
409: $sEventName = $aEvent['subject'];
410: $sEventText = $aEvent['description'];
411: $bAllDay = $aEvent['allDay'];
412: $sDate = $aUserEvent['time'];
413:
414: if (isset($vCal->getBaseComponent('VEVENT')->RRULE) && $iEventStartTS < $iNowTS) { // the correct date for repeatable events
415: $aBaseEvents = $vCal->getBaseComponents('VEVENT');
416: if (isset($aBaseEvents[0])) {
417: $oEventStartDT = \Aurora\Modules\Calendar\Classes\Helper::getNextRepeat($oNowDT_UTC, $aBaseEvents[0]);
418: if ($oEventStartDT) {
419: $oEventStartDT = $oEventStartDT->setTimezone(new \DateTimeZone($oUser->DefaultTimeZone ?: 'UTC'));
420: $sEventStart = $oEventStartDT->format('Y-m-d H:i:s');
421: if ($bAllDay) {
422: $sDate = $oEventStartDT->format('d m Y');
423: } else {
424: $sDate = $oEventStartDT->format('d m Y H:i');
425: }
426: $iEventStartTS = $oEventStartDT->getTimestamp();
427: }
428: }
429: }
430:
431: $sSubject = $this->getSubject($oUser, $sEventStart, $iEventStartTS, $sEventName, $sDate, $iNowTS, $bAllDay);
432:
433: $aUsers = [
434: $oUser->Id => $oUser
435: ];
436:
437: // TODO: get users to whom the calendar is shared
438:
439: // $aCalendarUsers = $this->oCalendarManager->getCalendarUsers($oUser->PublicId, $oCalendar);
440: // if (0 < count($aCalendarUsers)) {
441: // foreach ($aCalendarUsers as $aCalendarUser) {
442: // $oCalendarUser = $this->getUser($aCalendarUser['email']);
443: // if ($oCalendarUser) {
444: // $aUsers[$oCalendarUser->Id] = $oCalendarUser;
445: // }
446: // }
447: // }
448:
449: foreach ($aUsers as $oUserItem) {
450: $bIsMessageSent = false;
451: $oEvent = $this->oCalendarManager->getEvent($sEmail, $sEventCalendarId, $sEventId);
452: if ($oEvent) {
453: \Aurora\System\Api::Log('Send reminder - calendar: \'' . $sEventCalendarId . '\', event: \'' . $sEventName . '\' started on \'' . $sDate . '\' to \'' . $oUserItem->PublicId . '\'', \Aurora\System\Enums\LogLevel::Full, 'cron-');
454: $bIsMessageSent = $this->sendMessage($oUserItem, $sSubject, $sEventName, $sDate, $oCalendar->DisplayName, $sEventText, $oCalendar->Color);
455:
456: $aArgs = array(
457: array(
458: "Email" => $sEmail,
459: "Debug" => false,
460: "Data" => array(
461: array(
462: "Type" => $aEvent['type'] === 'VTODO' ? 'task' : 'event',
463: "From" => "",
464: "To" => $sEmail,
465: "Subject" => $sSubject,
466: "EventId" => $sEventId,
467: "CalendarId" => $sEventCalendarId
468: )
469: )
470: )
471: );
472:
473: $bResult = false;
474:
475: $this->oCalendarModule->broadcastEvent(
476: 'SendNotification',
477: $aArgs,
478: $bResult
479: );
480: } else {
481: \Aurora\System\Api::Log('Event not found - User: ' . $sEmail . ', Calendar: ' . $sCalendarUri . ' , Event: ' . $sEventId, \Aurora\System\Enums\LogLevel::Full, 'cron-');
482: }
483: if ($bIsMessageSent) {
484: $sEventUrl = (substr(strtolower($sEventId), -4) !== '.ics') ? $sEventId . '.ics' : $sEventId;
485: $this->oCalendarManager->updateReminder($oUserItem->PublicId, $sEventCalendarId, $sEventUrl, $vCal->serialize());
486: } else {
487: \Aurora\System\Api::Log('Send reminder for event: FAILED!', \Aurora\System\Enums\LogLevel::Full, 'cron-');
488: }
489: }
490: } else {
491: \Aurora\System\Api::Log('Calendar ' . $sCalendarUri . ' not found for user \'' . $oUser->PublicId . '\'', \Aurora\System\Enums\LogLevel::Full, 'cron-');
492: }
493: }
494: }
495: }
496: }
497: }
498: }
499:
500: $this->DeleteOutdatedReminders($iStartTS);
501: file_put_contents($this->sCurRunFilePath, $iNowTS);
502: }
503:
504: \Aurora\System\Api::Log('---------- End cron script', \Aurora\System\Enums\LogLevel::Full, 'cron-');
505: }
506: }
507:
508: $iTimer = microtime(true);
509:
510: Reminder::NewInstance()->Execute();
511:
512: \Aurora\System\Api::Log('Cron execution time: ' . (microtime(true) - $iTimer) . ' sec.', \Aurora\System\Enums\LogLevel::Full, 'cron-');
513: