4
|
1 <?php
|
|
2
|
|
3 /**
|
|
4 * iTIP functions for the calendar-based Roudncube plugins
|
|
5 *
|
|
6 * Class providing functionality to manage iTIP invitations
|
|
7 *
|
|
8 * @author Thomas Bruederli <bruederli@kolabsys.com>
|
|
9 *
|
|
10 * Copyright (C) 2011-2014, Kolab Systems AG <contact@kolabsys.com>
|
|
11 *
|
|
12 * This program is free software: you can redistribute it and/or modify
|
|
13 * it under the terms of the GNU Affero General Public License as
|
|
14 * published by the Free Software Foundation, either version 3 of the
|
|
15 * License, or (at your option) any later version.
|
|
16 *
|
|
17 * This program is distributed in the hope that it will be useful,
|
|
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
20 * GNU Affero General Public License for more details.
|
|
21 *
|
|
22 * You should have received a copy of the GNU Affero General Public License
|
|
23 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
24 */
|
|
25 class libcalendaring_itip
|
|
26 {
|
|
27 protected $rc;
|
|
28 protected $lib;
|
|
29 protected $plugin;
|
|
30 protected $sender;
|
|
31 protected $domain;
|
|
32 protected $itip_send = false;
|
|
33 protected $rsvp_actions = array('accepted','tentative','declined','delegated');
|
|
34 protected $rsvp_status = array('accepted','tentative','declined','delegated');
|
|
35
|
|
36 function __construct($plugin, $domain = 'libcalendaring')
|
|
37 {
|
|
38 $this->plugin = $plugin;
|
|
39 $this->rc = rcube::get_instance();
|
|
40 $this->lib = libcalendaring::get_instance();
|
|
41 $this->domain = $domain;
|
|
42
|
|
43 $hook = $this->rc->plugins->exec_hook('calendar_load_itip',
|
|
44 array('identity' => $this->rc->user->list_emails(true)));
|
|
45 $this->sender = $hook['identity'];
|
|
46
|
|
47 $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook'));
|
|
48 $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook'));
|
|
49 }
|
|
50
|
|
51 public function set_sender_email($email)
|
|
52 {
|
|
53 if (!empty($email))
|
|
54 $this->sender['email'] = $email;
|
|
55 }
|
|
56
|
|
57 public function set_rsvp_actions($actions)
|
|
58 {
|
|
59 $this->rsvp_actions = (array)$actions;
|
|
60 $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
|
|
61 }
|
|
62
|
|
63 public function set_rsvp_status($status)
|
|
64 {
|
|
65 $this->rsvp_status = $status;
|
|
66 }
|
|
67
|
|
68 /**
|
|
69 * Wrapper for rcube_plugin::gettext()
|
|
70 * Checking for a label in different domains
|
|
71 *
|
|
72 * @see rcube::gettext()
|
|
73 */
|
|
74 public function gettext($p)
|
|
75 {
|
|
76 $label = is_array($p) ? $p['name'] : $p;
|
|
77 $domain = $this->domain;
|
|
78 if (!$this->rc->text_exists($label, $domain)) {
|
|
79 $domain = 'libcalendaring';
|
|
80 }
|
|
81 return $this->rc->gettext($p, $domain);
|
|
82 }
|
|
83
|
|
84 /**
|
|
85 * Send an iTip mail message
|
|
86 *
|
|
87 * @param array Event object to send
|
|
88 * @param string iTip method (REQUEST|REPLY|CANCEL)
|
|
89 * @param array Hash array with recipient data (name, email)
|
|
90 * @param string Mail subject
|
|
91 * @param string Mail body text label
|
|
92 * @param object Mail_mime object with message data
|
|
93 * @param boolean Request RSVP
|
|
94 * @return boolean True on success, false on failure
|
|
95 */
|
|
96 public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
|
|
97 {
|
|
98 if (!$this->sender['name'])
|
|
99 $this->sender['name'] = $this->sender['email'];
|
|
100
|
|
101 if (!$message) {
|
|
102 libcalendaring::identify_recurrence_instance($event);
|
|
103 $message = $this->compose_itip_message($event, $method, $rsvp);
|
|
104 }
|
|
105
|
|
106 $mailto = rcube_utils::idn_to_ascii($recipient['email']);
|
|
107
|
|
108 $headers = $message->headers();
|
|
109 $headers['To'] = format_email_recipient($mailto, $recipient['name']);
|
|
110 $headers['Subject'] = $this->gettext(array(
|
|
111 'name' => $subject,
|
|
112 'vars' => array(
|
|
113 'title' => $event['title'],
|
|
114 'name' => $this->sender['name']
|
|
115 )
|
|
116 ));
|
|
117
|
|
118 // compose a list of all event attendees
|
|
119 $attendees_list = array();
|
|
120 foreach ((array)$event['attendees'] as $attendee) {
|
|
121 $attendees_list[] = ($attendee['name'] && $attendee['email']) ?
|
|
122 $attendee['name'] . ' <' . $attendee['email'] . '>' :
|
|
123 ($attendee['name'] ? $attendee['name'] : $attendee['email']);
|
|
124 }
|
|
125
|
|
126 $recurrence_info = '';
|
|
127 if (!empty($event['recurrence_id'])) {
|
|
128 $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
|
|
129 }
|
|
130 else if (!empty($event['recurrence'])) {
|
|
131 $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
|
|
132 }
|
|
133
|
|
134 $mailbody = $this->gettext(array(
|
|
135 'name' => $bodytext,
|
|
136 'vars' => array(
|
|
137 'title' => $event['title'],
|
|
138 'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
|
|
139 'attendees' => join(",\n ", $attendees_list),
|
|
140 'sender' => $this->sender['name'],
|
|
141 'organizer' => $this->sender['name'],
|
|
142 )
|
|
143 ));
|
|
144
|
|
145 // if (!empty($event['comment'])) {
|
|
146 // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
|
|
147 // }
|
|
148
|
|
149 // append links for direct invitation replies
|
|
150 if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) {
|
|
151 $mailbody .= "\n\n" . $this->gettext(array(
|
|
152 'name' => 'invitationattendlinks',
|
|
153 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))),
|
|
154 ));
|
|
155 }
|
|
156 else if ($method == 'CANCEL' && $event['cancelled']) {
|
|
157 $this->cancel_itip_invitation($event);
|
|
158 }
|
|
159
|
|
160 $message->headers($headers, true);
|
|
161 $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
|
|
162
|
|
163 if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
|
|
164 rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $message->get());
|
|
165 }
|
|
166
|
|
167 // finally send the message
|
|
168 $this->itip_send = true;
|
|
169 $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
|
|
170 $this->itip_send = false;
|
|
171
|
|
172 return $sent;
|
|
173 }
|
|
174
|
|
175 /**
|
|
176 * Plugin hook triggered by rcube::deliver_message() before delivering a message.
|
|
177 * Here we can set the 'smtp_server' config option to '' in order to use
|
|
178 * PHP's mail() function for unauthenticated email sending.
|
|
179 */
|
|
180 public function before_send_hook($p)
|
|
181 {
|
|
182 if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') {
|
|
183 $this->rc->config->set('smtp_server', '');
|
|
184 }
|
|
185
|
|
186 return $p;
|
|
187 }
|
|
188
|
|
189 /**
|
|
190 * Plugin hook to alter SMTP authentication.
|
|
191 * This is used if iTip messages are to be sent from an unauthenticated session
|
|
192 */
|
|
193 public function smtp_connect_hook($p)
|
|
194 {
|
|
195 // replace smtp auth settings if we're not in an authenticated session
|
|
196 if ($this->itip_send && !$this->rc->user->ID) {
|
|
197 foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) {
|
|
198 $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]);
|
|
199 }
|
|
200 }
|
|
201
|
|
202 return $p;
|
|
203 }
|
|
204
|
|
205 /**
|
|
206 * Helper function to build a Mail_mime object to send an iTip message
|
|
207 *
|
|
208 * @param array Event object to send
|
|
209 * @param string iTip method (REQUEST|REPLY|CANCEL)
|
|
210 * @param boolean Request RSVP
|
|
211 * @return object Mail_mime object with message data
|
|
212 */
|
|
213 public function compose_itip_message($event, $method, $rsvp = true)
|
|
214 {
|
|
215 $from = rcube_utils::idn_to_ascii($this->sender['email']);
|
|
216 $from_utf = rcube_utils::idn_to_utf8($from);
|
|
217 $sender = format_email_recipient($from, $this->sender['name']);
|
|
218
|
|
219 // truncate list attendees down to the recipient of the iTip Reply.
|
|
220 // constraints for a METHOD:REPLY according to RFC 5546
|
|
221 if ($method == 'REPLY') {
|
|
222 $replying_attendee = null;
|
|
223 $reply_attendees = array();
|
|
224 foreach ($event['attendees'] as $attendee) {
|
|
225 if ($attendee['role'] == 'ORGANIZER') {
|
|
226 $reply_attendees[] = $attendee;
|
|
227 }
|
|
228 else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
|
|
229 $replying_attendee = $attendee;
|
|
230 if ($attendee['status'] != 'DELEGATED') {
|
|
231 unset($replying_attendee['rsvp']); // unset the RSVP attribute
|
|
232 }
|
|
233 }
|
|
234 // include attendees relevant for delegation (RFC 5546, Section 4.2.5)
|
|
235 else if ((!empty($attendee['delegated-to']) &&
|
|
236 (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
|
|
237 (!empty($attendee['delegated-from']) &&
|
|
238 (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
|
|
239 $reply_attendees[] = $attendee;
|
|
240 }
|
|
241 }
|
|
242 if ($replying_attendee) {
|
|
243 array_unshift($reply_attendees, $replying_attendee);
|
|
244 $event['attendees'] = $reply_attendees;
|
|
245 }
|
|
246 if ($event['recurrence']) {
|
|
247 unset($event['recurrence']['EXCEPTIONS']);
|
|
248 }
|
|
249 }
|
|
250 // set RSVP for every attendee
|
|
251 else if ($method == 'REQUEST') {
|
|
252 foreach ($event['attendees'] as $i => $attendee) {
|
|
253 if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
|
|
254 $event['attendees'][$i]['rsvp']= (bool)$rsvp;
|
|
255 }
|
|
256 }
|
|
257 }
|
|
258 else if ($method == 'CANCEL') {
|
|
259 if ($event['recurrence']) {
|
|
260 unset($event['recurrence']['EXCEPTIONS']);
|
|
261 }
|
|
262 }
|
|
263
|
|
264 // Set SENT-BY property if the sender is not the organizer
|
|
265 if ($method == 'CANCEL' || $method == 'REQUEST') {
|
|
266 foreach ((array)$event['attendees'] as $idx => $attendee) {
|
|
267 if ($attendee['role'] == 'ORGANIZER'
|
|
268 && $attendee['email']
|
|
269 && strcasecmp($attendee['email'], $from) != 0
|
|
270 && strcasecmp($attendee['email'], $from_utf) != 0
|
|
271 ) {
|
|
272 $attendee['sent-by'] = 'mailto:' . $from_utf;
|
|
273 $event['organizer'] = $event['attendees'][$idx] = $attendee;
|
|
274 break;
|
|
275 }
|
|
276 }
|
|
277 }
|
|
278
|
|
279 // compose multipart message using PEAR:Mail_Mime
|
|
280 $message = new Mail_mime("\r\n");
|
|
281 $message->setParam('text_encoding', 'quoted-printable');
|
|
282 $message->setParam('head_encoding', 'quoted-printable');
|
|
283 $message->setParam('head_charset', RCUBE_CHARSET);
|
|
284 $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
|
|
285 $message->setContentType('multipart/alternative');
|
|
286
|
|
287 // compose common headers array
|
|
288 $headers = array(
|
|
289 'From' => $sender,
|
|
290 'Date' => $this->rc->user_date(),
|
|
291 'Message-ID' => $this->rc->gen_message_id(),
|
|
292 'X-Sender' => $from,
|
|
293 );
|
|
294 if ($agent = $this->rc->config->get('useragent')) {
|
|
295 $headers['User-Agent'] = $agent;
|
|
296 }
|
|
297
|
|
298 $message->headers($headers);
|
|
299
|
|
300 // attach ics file for this event
|
|
301 $ical = libcalendaring::get_ical();
|
|
302 $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
|
|
303 $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
|
|
304 $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method);
|
|
305
|
|
306 return $message;
|
|
307 }
|
|
308
|
|
309 /**
|
|
310 * Forward the given iTip event as delegation to another person
|
|
311 *
|
|
312 * @param array Event object to delegate
|
|
313 * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
|
|
314 * @param boolean The delegator's RSVP flag
|
|
315 * @param array List with indexes of new/updated attendees
|
|
316 * @return boolean True on success, False on failure
|
|
317 */
|
|
318 public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
|
|
319 {
|
|
320 if (is_string($delegate)) {
|
|
321 $delegates = rcube_mime::decode_address_list($delegate, 1, false);
|
|
322 if (count($delegates) > 0) {
|
|
323 $delegate = reset($delegates);
|
|
324 }
|
|
325 }
|
|
326
|
|
327 $emails = $this->lib->get_user_emails();
|
|
328 $me = $this->rc->user->list_emails(true);
|
|
329
|
|
330 // find/create the delegate attendee
|
|
331 $delegate_attendee = array(
|
|
332 'email' => $delegate['mailto'],
|
|
333 'name' => $delegate['name'],
|
|
334 'role' => 'REQ-PARTICIPANT',
|
|
335 );
|
|
336 $delegate_index = count($event['attendees']);
|
|
337
|
|
338 foreach ($event['attendees'] as $i => $attendee) {
|
|
339 // set myself the DELEGATED-TO parameter
|
|
340 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
|
341 $event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
|
|
342 $event['attendees'][$i]['status'] = 'DELEGATED';
|
|
343 $event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
|
|
344 $event['attendees'][$i]['rsvp'] = $rsvp;
|
|
345
|
|
346 $me['email'] = $attendee['email'];
|
|
347 $delegate_attendee['role'] = $attendee['role'];
|
|
348 }
|
|
349 // the disired delegatee is already listed as an attendee
|
|
350 else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
|
|
351 $delegate_attendee = $attendee;
|
|
352 $delegate_index = $i;
|
|
353 break;
|
|
354 }
|
|
355 // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
|
|
356 }
|
|
357
|
|
358 // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
|
|
359 $delegate_attendee['rsvp'] = true;
|
|
360 $delegate_attendee['status'] = 'NEEDS-ACTION';
|
|
361 $delegate_attendee['delegated-from'] = $me['email'];
|
|
362 $event['attendees'][$delegate_index] = $delegate_attendee;
|
|
363
|
|
364 $attendees[] = $delegate_index;
|
|
365
|
|
366 $this->set_sender_email($me['email']);
|
|
367 return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
|
|
368 }
|
|
369
|
|
370 /**
|
|
371 * Handler for calendar/itip-status requests
|
|
372 */
|
|
373 public function get_itip_status($event, $existing = null)
|
|
374 {
|
|
375 $action = $event['rsvp'] ? 'rsvp' : '';
|
|
376 $status = $event['fallback'];
|
|
377 $latest = $resheduled = false;
|
|
378 $html = '';
|
|
379
|
|
380 if (is_numeric($event['changed']))
|
|
381 $event['changed'] = new DateTime('@'.$event['changed']);
|
|
382
|
|
383 // check if the given itip object matches the last state
|
|
384 if ($existing) {
|
|
385 $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
|
|
386 (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
|
|
387 }
|
|
388
|
|
389 // determine action for REQUEST
|
|
390 if ($event['method'] == 'REQUEST') {
|
|
391 $html = html::div('rsvp-status', $this->gettext('acceptinvitation'));
|
|
392
|
|
393 if ($existing) {
|
|
394 $rsvp = $event['rsvp'];
|
|
395 $emails = $this->lib->get_user_emails();
|
|
396 foreach ($existing['attendees'] as $attendee) {
|
|
397 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
|
398 $status = strtoupper($attendee['status']);
|
|
399 break;
|
|
400 }
|
|
401 }
|
|
402
|
|
403 // Detect re-sheduling
|
|
404 if (!$latest) {
|
|
405 // FIXME: This is probably to simplistic, or maybe we should just check
|
|
406 // attendee's RSVP flag in the new event?
|
|
407 $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end'];
|
|
408 }
|
|
409 }
|
|
410 else {
|
|
411 $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true);
|
|
412 }
|
|
413
|
|
414 $status_lc = strtolower($status);
|
|
415
|
|
416 if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
|
|
417 $html = html::div('rsvp-status', $this->gettext('notanattendee'));
|
|
418 $action = 'import';
|
|
419 }
|
|
420 else if (in_array($status_lc, $this->rsvp_status)) {
|
|
421 $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc);
|
|
422
|
|
423 if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) {
|
|
424 $action = ''; // nothing to do here, outdated invitation
|
|
425 if ($status_lc == 'needs-action')
|
|
426 $status_text = $this->gettext('outdatedinvitation');
|
|
427 }
|
|
428 else if (!$existing && !$rsvp) {
|
|
429 $action = 'import';
|
|
430 }
|
|
431 else if ($resheduled) {
|
|
432 $action = 'rsvp';
|
|
433 }
|
|
434 else if ($status_lc != 'needs-action') {
|
|
435 $action = !$latest ? 'update' : '';
|
|
436 }
|
|
437
|
|
438 $html = html::div('rsvp-status ' . $status_lc, $status_text);
|
|
439 }
|
|
440 }
|
|
441 // determine action for REPLY
|
|
442 else if ($event['method'] == 'REPLY') {
|
|
443 // check whether the sender already is an attendee
|
|
444 if ($existing) {
|
|
445 $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : '';
|
|
446 $listed = false;
|
|
447 foreach ($existing['attendees'] as $attendee) {
|
|
448 if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
|
|
449 $status_lc = strtolower($status);
|
|
450 if (in_array($status_lc, $this->rsvp_status)) {
|
|
451 $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
|
|
452 'name' => 'attendee' . $status_lc,
|
|
453 'vars' => array(
|
|
454 'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
|
|
455 )
|
|
456 )));
|
|
457 }
|
|
458 $action = $attendee['status'] == $status || !$latest ? '' : 'update';
|
|
459 $listed = true;
|
|
460 break;
|
|
461 }
|
|
462 }
|
|
463
|
|
464 if (!$listed) {
|
|
465 $html = html::div('rsvp-status', $this->gettext('itipnewattendee'));
|
|
466 }
|
|
467 }
|
|
468 else {
|
|
469 $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
|
|
470 $action = '';
|
|
471 }
|
|
472 }
|
|
473 else if ($event['method'] == 'CANCEL') {
|
|
474 if (!$existing) {
|
|
475 $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
|
|
476 $action = '';
|
|
477 }
|
|
478 }
|
|
479
|
|
480 return array(
|
|
481 'uid' => $event['uid'],
|
|
482 'id' => asciiwords($event['uid'], true),
|
|
483 'existing' => $existing ? true : false,
|
|
484 'saved' => $existing ? true : false,
|
|
485 'latest' => $latest,
|
|
486 'status' => $status,
|
|
487 'action' => $action,
|
|
488 'resheduled' => $resheduled,
|
|
489 'html' => $html,
|
|
490 );
|
|
491 }
|
|
492
|
|
493 /**
|
|
494 * Build inline UI elements for iTip messages
|
|
495 */
|
|
496 public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
|
|
497 {
|
|
498 $buttons = array();
|
|
499 $dom_id = asciiwords($event['uid'], true);
|
|
500 $rsvp_status = 'unknown';
|
|
501
|
|
502 // pass some metadata about the event and trigger the asynchronous status check
|
|
503 $changed = is_object($event['changed']) ? $event['changed'] : $message_date;
|
|
504 $metadata = array(
|
|
505 'uid' => $event['uid'],
|
|
506 '_instance' => $event['_instance'],
|
|
507 'changed' => $changed ? $changed->format('U') : 0,
|
|
508 'sequence' => intval($event['sequence']),
|
|
509 'method' => $method,
|
|
510 'task' => $task,
|
|
511 );
|
|
512
|
|
513 // create buttons to be activated from async request checking existence of this event in local calendars
|
|
514 $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
|
|
515
|
|
516 // on iTip REPLY we have two options:
|
|
517 if ($method == 'REPLY') {
|
|
518 $title = $this->gettext('itipreply');
|
|
519
|
|
520 foreach ($event['attendees'] as $attendee) {
|
|
521 if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
|
|
522 if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
|
|
523 $metadata['attendee'] = $attendee['email'];
|
|
524 $rsvp_status = strtoupper($attendee['status']);
|
|
525 if ($attendee['delegated-to']) {
|
|
526 $metadata['delegated-to'] = $attendee['delegated-to'];
|
|
527 }
|
|
528 break;
|
|
529 }
|
|
530 }
|
|
531 }
|
|
532
|
|
533 // 1. update the attendee status on our copy
|
|
534 $update_button = html::tag('input', array(
|
|
535 'type' => 'button',
|
|
536 'class' => 'button',
|
|
537 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
|
|
538 'value' => $this->gettext('updateattendeestatus'),
|
|
539 ));
|
|
540
|
|
541 // 2. accept or decline a new or delegate attendee
|
|
542 $accept_buttons = html::tag('input', array(
|
|
543 'type' => 'button',
|
|
544 'class' => "button accept",
|
|
545 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
|
|
546 'value' => $this->gettext('acceptattendee'),
|
|
547 ));
|
|
548 $accept_buttons .= html::tag('input', array(
|
|
549 'type' => 'button',
|
|
550 'class' => "button decline",
|
|
551 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . rcube::JQ($mime_id) . "', '$task')",
|
|
552 'value' => $this->gettext('declineattendee'),
|
|
553 ));
|
|
554
|
|
555 $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
|
|
556 $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons);
|
|
557 }
|
|
558 // when receiving iTip REQUEST messages:
|
|
559 else if ($method == 'REQUEST') {
|
|
560 $emails = $this->lib->get_user_emails();
|
|
561 $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
|
|
562 $metadata['rsvp'] = true;
|
|
563 $metadata['sensitivity'] = $event['sensitivity'];
|
|
564
|
|
565 if (is_object($event['start'])) {
|
|
566 $metadata['date'] = $event['start']->format('U');
|
|
567 }
|
|
568
|
|
569 // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons
|
|
570 if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') {
|
|
571 $this->rsvp_actions = array('accepted','declined');
|
|
572 $metadata['nosave'] = true;
|
|
573 }
|
|
574
|
|
575 // 1. display RSVP buttons (if the user was invited)
|
|
576 foreach ($this->rsvp_actions as $method) {
|
|
577 $rsvp_buttons .= html::tag('input', array(
|
|
578 'type' => 'button',
|
|
579 'class' => "button $method",
|
|
580 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task', '$method', '$dom_id')",
|
|
581 'value' => $this->gettext('itip' . $method),
|
|
582 ));
|
|
583 }
|
|
584
|
|
585 // add button to open calendar/preview
|
|
586 if (!empty($preview_url)) {
|
|
587 $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id;
|
|
588 $rsvp_buttons .= html::tag('input', array(
|
|
589 'type' => 'button',
|
|
590 'class' => "button preview",
|
|
591 'onclick' => "rcube_libcalendaring.open_itip_preview('" . rcube::JQ($preview_url) . "', '" . rcube::JQ($msgref) . "')",
|
|
592 'value' => $this->gettext('openpreview'),
|
|
593 ));
|
|
594 }
|
|
595
|
|
596 // 2. update the local copy with minor changes
|
|
597 $update_button = html::tag('input', array(
|
|
598 'type' => 'button',
|
|
599 'class' => 'button',
|
|
600 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
|
|
601 'value' => $this->gettext('updatemycopy'),
|
|
602 ));
|
|
603
|
|
604 // 3. Simply import the event without replying
|
|
605 $import_button = html::tag('input', array(
|
|
606 'type' => 'button',
|
|
607 'class' => 'button',
|
|
608 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
|
|
609 'value' => $this->gettext('importtocalendar'),
|
|
610 ));
|
|
611
|
|
612 // check my status
|
|
613 foreach ($event['attendees'] as $attendee) {
|
|
614 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
|
615 $metadata['attendee'] = $attendee['email'];
|
|
616 $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
|
|
617 $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
|
|
618 break;
|
|
619 }
|
|
620 }
|
|
621
|
|
622 // add itip reply message controls
|
|
623 $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
|
|
624
|
|
625 $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
|
|
626 $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
|
|
627
|
|
628 // prepare autocompletion for delegation dialog
|
|
629 if (in_array('delegated', $this->rsvp_actions)) {
|
|
630 $this->rc->autocomplete_init();
|
|
631 }
|
|
632 }
|
|
633 // for CANCEL messages, we can:
|
|
634 else if ($method == 'CANCEL') {
|
|
635 $title = $this->gettext('itipcancellation');
|
|
636 $event_prop = array_filter(array(
|
|
637 'uid' => $event['uid'],
|
|
638 '_instance' => $event['_instance'],
|
|
639 '_savemode' => $event['_savemode'],
|
|
640 ));
|
|
641
|
|
642 // 1. remove the event from our calendar
|
|
643 $button_remove = html::tag('input', array(
|
|
644 'type' => 'button',
|
|
645 'class' => 'button',
|
|
646 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . rcube::JQ($event['title']) . "')",
|
|
647 'value' => $this->gettext('removefromcalendar'),
|
|
648 ));
|
|
649
|
|
650 // 2. update our copy with status=cancelled
|
|
651 $button_update = html::tag('input', array(
|
|
652 'type' => 'button',
|
|
653 'class' => 'button',
|
|
654 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
|
|
655 'value' => $this->gettext('updatemycopy'),
|
|
656 ));
|
|
657
|
|
658 $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update);
|
|
659
|
|
660 $rsvp_status = 'CANCELLED';
|
|
661 $metadata['rsvp'] = true;
|
|
662 }
|
|
663
|
|
664 // append generic import button
|
|
665 if ($import_button) {
|
|
666 $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
|
|
667 }
|
|
668
|
|
669 // pass some metadata about the event and trigger the asynchronous status check
|
|
670 $metadata['fallback'] = $rsvp_status;
|
|
671 $metadata['rsvp'] = intval($metadata['rsvp']);
|
|
672
|
|
673 $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . rcube_output::json_serialize($metadata) . ")", 'docready');
|
|
674
|
|
675 // get localized texts from the right domain
|
|
676 foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
|
|
677 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
|
|
678 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
|
|
679 $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
|
|
680 }
|
|
681
|
|
682 // show event details with buttons
|
|
683 return $this->itip_object_details_table($event, $title) .
|
|
684 html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons));
|
|
685 }
|
|
686
|
|
687 /**
|
|
688 * Render an RSVP UI widget with buttons to respond on iTip invitations
|
|
689 */
|
|
690 function itip_rsvp_buttons($attrib = array(), $actions = null)
|
|
691 {
|
|
692 $attrib += array('type' => 'button');
|
|
693
|
|
694 if (!$actions)
|
|
695 $actions = $this->rsvp_actions;
|
|
696
|
|
697 foreach ($actions as $method) {
|
|
698 $buttons .= html::tag('input', array(
|
|
699 'type' => $attrib['type'],
|
|
700 'name' => $attrib['iname'],
|
|
701 'class' => 'button',
|
|
702 'rel' => $method,
|
|
703 'value' => $this->gettext('itip' . $method),
|
|
704 ));
|
|
705 }
|
|
706
|
|
707 // add localized texts for the delegation dialog
|
|
708 if (in_array('delegated', $actions)) {
|
|
709 foreach (array('itipdelegated','itipcomment','delegateinvitation',
|
|
710 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
|
|
711 $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
|
|
712 }
|
|
713 }
|
|
714
|
|
715 foreach (array('all','current','future') as $mode) {
|
|
716 $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
|
|
717 }
|
|
718
|
|
719 $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
|
|
720
|
|
721 return html::div($attrib,
|
|
722 html::div('label', $this->gettext('acceptinvitation')) .
|
|
723 html::div('rsvp-buttons',
|
|
724 $buttons .
|
|
725 html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
|
|
726 )
|
|
727 );
|
|
728 }
|
|
729
|
|
730 /**
|
|
731 * Render UI elements to control iTip reply message sending
|
|
732 */
|
|
733 public function itip_rsvp_options_ui($dom_id, $disable = false)
|
|
734 {
|
|
735 $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3);
|
|
736
|
|
737 // itip sending is entirely disabled
|
|
738 if ($itip_sending === 0) {
|
|
739 return '';
|
|
740 }
|
|
741 // add checkbox to suppress itip reply message
|
|
742 else if ($itip_sending >= 2) {
|
|
743 $rsvp_additions = html::label(array('class' => 'noreply-toggle'),
|
|
744 html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0))
|
|
745 . ' ' . $this->gettext('itipsuppressreply')
|
|
746 );
|
|
747 }
|
|
748
|
|
749 // add input field for reply comment
|
|
750 $toggle_attrib = array(
|
|
751 'href' => '#toggle',
|
|
752 'class' => 'reply-comment-toggle',
|
|
753 'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()'
|
|
754 );
|
|
755 $textarea_attrib = array(
|
|
756 'id' => 'reply-comment-' . $dom_id,
|
|
757 'name' => '_comment',
|
|
758 'cols' => 40,
|
|
759 'rows' => 6,
|
|
760 'style' => 'display:none',
|
|
761 'placeholder' => $this->gettext('itipcomment')
|
|
762 );
|
|
763
|
|
764 $rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse'))
|
|
765 . html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, ''));
|
|
766
|
|
767 return $rsvp_additions;
|
|
768 }
|
|
769
|
|
770 /**
|
|
771 * Render event/task details in a table
|
|
772 */
|
|
773 function itip_object_details_table($event, $title)
|
|
774 {
|
|
775 $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
|
|
776 $table->add('ititle', $title);
|
|
777 $table->add('title', rcube::Q($event['title']));
|
|
778 if ($event['start'] && $event['end']) {
|
|
779 $table->add('label', $this->gettext('date'));
|
|
780 $table->add('date', rcube::Q($this->lib->event_date_text($event)));
|
|
781 }
|
|
782 else if ($event['due'] && $event['_type'] == 'task') {
|
|
783 $table->add('label', $this->gettext('date'));
|
|
784 $table->add('date', rcube::Q($this->lib->event_date_text($event)));
|
|
785 }
|
|
786 if (!empty($event['recurrence_date'])) {
|
|
787 $table->add('label', '');
|
|
788 $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
|
|
789 }
|
|
790 else if (!empty($event['recurrence'])) {
|
|
791 $table->add('label', $this->gettext('recurring'));
|
|
792 $table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
|
|
793 }
|
|
794 if ($event['location']) {
|
|
795 $table->add('label', $this->gettext('location'));
|
|
796 $table->add('location', rcube::Q($event['location']));
|
|
797 }
|
|
798 if ($event['sensitivity'] && $event['sensitivity'] != 'public') {
|
|
799 $table->add('label', $this->gettext('sensitivity'));
|
|
800 $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!');
|
|
801 }
|
|
802 if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
|
|
803 $table->add('label', $this->gettext('status'));
|
|
804 $table->add('status', $this->gettext('status-' . strtolower($event['status'])));
|
|
805 }
|
|
806 if ($event['comment']) {
|
|
807 $table->add('label', $this->gettext('comment'));
|
|
808 $table->add('location', rcube::Q($event['comment']));
|
|
809 }
|
|
810
|
|
811 return $table->show();
|
|
812 }
|
|
813
|
|
814
|
|
815 /**
|
|
816 * Create iTIP invitation token for later replies via URL
|
|
817 *
|
|
818 * @param array Hash array with event properties
|
|
819 * @param string Attendee email address
|
|
820 * @return string Invitation token
|
|
821 */
|
|
822 public function store_invitation($event, $attendee)
|
|
823 {
|
|
824 // empty stub
|
|
825 return false;
|
|
826 }
|
|
827
|
|
828 /**
|
|
829 * Mark invitations for the given event as cancelled
|
|
830 *
|
|
831 * @param array Hash array with event properties
|
|
832 */
|
|
833 public function cancel_itip_invitation($event)
|
|
834 {
|
|
835 // empty stub
|
|
836 return false;
|
|
837 }
|
|
838
|
|
839 /**
|
|
840 * Utility function to get the value of a custom property
|
|
841 */
|
|
842 public static function get_custom_property($event, $name)
|
|
843 {
|
|
844 $ret = false;
|
|
845
|
|
846 if (is_array($event['x-custom'])) {
|
|
847 array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) {
|
|
848 if (strcasecmp($prop[0], $name) === 0) {
|
|
849 $ret = $prop[1];
|
|
850 }
|
|
851 });
|
|
852 }
|
|
853
|
|
854 return $ret;
|
|
855 }
|
|
856
|
|
857 /**
|
|
858 * Compare email address
|
|
859 */
|
|
860 public static function compare_email($value, $email, $email_utf = null)
|
|
861 {
|
|
862 $v1 = !empty($email) && strcasecmp($value, $email) === 0;
|
|
863 $v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0;
|
|
864
|
|
865 return $v1 || $v2;
|
|
866 }
|
|
867 }
|