1/* UIxMailPartICalActions.m - this file is part of SOGo
2 *
3 * Copyright (C) 2007-2016 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#import <Foundation/NSCalendarDate.h>
22#import <Foundation/NSDictionary.h>
23#import <Foundation/NSValue.h>
24
25#import <NGObjWeb/NSException+HTTP.h>
26#import <NGObjWeb/WOContext+SoObjects.h>
27#import <NGObjWeb/WORequest.h>
28#import <NGObjWeb/WOResponse.h>
29
30#import <NGExtensions/NSNull+misc.h>
31#import <NGExtensions/NSObject+Logs.h>
32
33#import <NGCards/iCalCalendar.h>
34
35#import <NGImap4/NGImap4EnvelopeAddress.h>
36
37#import <Appointments/iCalEvent+SOGo.h>
38#import <Appointments/iCalEntityObject+SOGo.h>
39#import <Appointments/iCalPerson+SOGo.h>
40#import <Appointments/SOGoAppointmentObject.h>
41#import <Appointments/SOGoAppointmentFolder.h>
42#import <Appointments/SOGoAppointmentFolders.h>
43#import <Mailer/SOGoMailObject.h>
44#import <SOGo/SOGoUser.h>
45#import <SOGo/SOGoUserManager.h>
46#import <SOGo/NSString+Utilities.h>
47#import <Mailer/SOGoMailBodyPart.h>
48
49#import "UIxMailPartICalActions.h"
50
51@implementation UIxMailPartICalActions
52
53- (iCalEvent *) _emailEvent
54{
55  iCalCalendar *emailCalendar;
56  NSString *eventString;
57  NSData *content;
58
59  content = [[self clientObject] fetchBLOB];
60  eventString = [[NSString alloc] initWithData: content
61				  encoding: NSUTF8StringEncoding];
62  if (!eventString)
63    eventString = [[NSString alloc] initWithData: content
64				    encoding: NSISOLatin1StringEncoding];
65
66  emailCalendar = [iCalCalendar parseSingleFromSource: eventString];
67  [eventString release];
68
69  return (iCalEvent *) [emailCalendar firstChildWithTag: @"vevent"];
70}
71
72//
73//
74//
75- (SOGoAppointmentObject *) _eventObjectWithUID: (NSString *) uid
76					forUser: (SOGoUser *) user
77{
78  SOGoAppointmentFolder *folder;
79  SOGoAppointmentObject *eventObject;
80  NSArray *folders;
81  NSEnumerator *e;
82  NSString *cname, *userLogin;
83
84  eventObject = nil;
85
86  userLogin = [user login];
87
88  folders = [[user calendarsFolderInContext: context] subFolders];
89  e = [folders objectEnumerator];
90  while (eventObject == nil && (folder = [e nextObject]))
91    {
92      if ([[folder ownerInContext: nil] isEqualToString: userLogin])
93        {
94          cname = [folder resourceNameForEventUID: uid];
95          if (cname)
96            {
97              eventObject = [folder lookupName: cname inContext: context
98                                       acquire: NO];
99              if ([eventObject isKindOfClass: [NSException class]])
100                eventObject = nil;
101            }
102        }
103    }
104
105  if (!eventObject)
106    {
107      folder = [user personalCalendarFolderInContext: context];
108      eventObject = [SOGoAppointmentObject objectWithName: uid
109					   inContainer: folder];
110      [eventObject setIsNew: YES];
111    }
112
113  return eventObject;
114}
115
116//
117//
118//
119- (SOGoAppointmentObject *) _eventObjectWithUID: (NSString *) uid
120{
121#warning this will not work if Bob reads emails of Alice and accepts events for her
122  return [self _eventObjectWithUID: uid forUser: [context activeUser]];
123}
124
125//
126//
127//
128- (void) _fixOrganizerInEvent: (iCalEvent *) brokenEvent
129{
130  iCalPerson *organizer;
131  SOGoMailObject *mail;
132  NSArray *addresses;
133  NGImap4EnvelopeAddress *from;
134  id value;
135
136  organizer = nil;
137
138  mail = [[self clientObject] mailObject];
139
140  addresses = [mail replyToEnvelopeAddresses];
141  if (![addresses count])
142    addresses = [mail fromEnvelopeAddresses];
143  if ([addresses count] > 0)
144    {
145      from = [addresses objectAtIndex: 0];
146      value = [from baseEMail];
147      if ([value isNotNull])
148	{
149	  organizer = [iCalPerson elementWithTag: @"organizer"];
150	  [organizer setEmail: value];
151	  value = [from personalName];
152	  if ([value isNotNull])
153	    [organizer setCn: value];
154	  [brokenEvent setOrganizer: organizer];
155	}
156    }
157
158  if (!organizer)
159    [self errorWithFormat: @"no organizer could be found, SOGo will crash"
160	  @" if the user replies"];
161}
162
163//
164//
165//
166- (iCalEvent *) _setupChosenEventAndEventObject: (SOGoAppointmentObject **) eventObject
167{
168  iCalEvent *emailEvent, *calendarEvent, *chosenEvent;
169  iCalPerson *organizer;
170
171  emailEvent = [self _emailEvent];
172  if (emailEvent)
173    {
174      *eventObject = [self _eventObjectWithUID: [emailEvent uid]];
175      if ([*eventObject isNew])
176	{
177	  chosenEvent = emailEvent;
178	  [*eventObject saveCalendar: [emailEvent parent]];
179	}
180      else
181	{
182	  if ([emailEvent recurrenceId])
183	    {
184	      // Event attached to email is not complete -- retrieve it
185	      // from the database.
186	      NSString *recurrenceTime;
187
188	      recurrenceTime = [NSString stringWithFormat: @"%f",
189					 [[emailEvent recurrenceId] timeIntervalSince1970]];
190	      calendarEvent = (iCalEvent *)[*eventObject lookupOccurrence: recurrenceTime];
191	    }
192	  else
193	    calendarEvent = (iCalEvent *) [*eventObject component: NO  secure: NO];
194
195	  if (calendarEvent != nil)
196	    {
197	      // Calendar event still exists -- verify which of the calendar
198	      // and email events is the most recent. We must also update
199	      // the event (or recurrence-id) with the email's content, otherwise
200	      // we would never get major properties updates
201	      if ([calendarEvent compare: emailEvent] == NSOrderedAscending)
202		{
203		  iCalCalendar *parent;
204
205		  parent = [calendarEvent parent];
206		  [parent removeChild: calendarEvent];
207		  [parent addChild: emailEvent];
208		  [*eventObject saveCalendar: parent];
209		  [*eventObject flush];
210		  chosenEvent = emailEvent;
211		}
212	      else
213		{
214		  chosenEvent = calendarEvent;
215		  if (![[[chosenEvent parent] method] length])
216		    [[chosenEvent parent] setMethod: [[emailEvent parent] method]];
217		}
218	    }
219	  else
220	    chosenEvent = emailEvent;
221	}
222
223      organizer = [chosenEvent organizer];
224      if (![[organizer rfc822Email] length])
225	[self _fixOrganizerInEvent: chosenEvent];
226    }
227  else
228    chosenEvent = nil;
229
230  return chosenEvent;
231}
232
233//
234//
235//
236- (WOResponse *) _changePartStatusAction: (NSString *) newStatus
237                            withDelegate: (iCalPerson *) delegate
238{
239  SOGoAppointmentObject *eventObject;
240  WOResponse *response;
241  iCalEvent *chosenEvent;
242  iCalAlarm *alarm;
243
244  chosenEvent = [self _setupChosenEventAndEventObject: &eventObject];
245  if (chosenEvent)
246    {
247      // For invitations, we take the organizers's alarm to start with
248      alarm = [[chosenEvent alarms] lastObject];
249      response = (WOResponse*)[eventObject changeParticipationStatus: newStatus
250							withDelegate: delegate
251                                                               alarm: alarm
252						     forRecurrenceId: [chosenEvent recurrenceId]];
253      if (!response)
254	response = [self responseWith204];
255    }
256  else
257    {
258      response = [context response];
259      [response setStatus: 409];
260    }
261
262  return response;
263}
264
265- (WOResponse *) acceptAction
266{
267  return [self _changePartStatusAction: @"ACCEPTED"
268			  withDelegate: nil];
269}
270
271- (WOResponse *) declineAction
272{
273  return [self _changePartStatusAction: @"DECLINED"
274			  withDelegate: nil];
275}
276
277- (WOResponse *) tentativeAction
278{
279  return [self _changePartStatusAction: @"TENTATIVE"
280			  withDelegate: nil];
281}
282
283- (WOResponse *) delegateAction
284{
285//  BOOL receiveUpdates;
286  NSString *delegatedEmail, *delegatedUid;
287  iCalPerson *delegatedAttendee;
288  NSDictionary *content;
289  SOGoUser *user;
290  WORequest *request;
291  WOResponse *response;
292
293  request = [context request];
294  content = [[request contentAsString] objectFromJSONString];
295  delegatedEmail = [content objectForKey: @"delegatedTo"];
296
297  if ([delegatedEmail length])
298    {
299      user = [context activeUser];
300      delegatedAttendee = [iCalPerson new];
301      [delegatedAttendee autorelease];
302      [delegatedAttendee setEmail: delegatedEmail];
303      delegatedUid = [delegatedAttendee uidInDomain: [user domain]];
304      if (delegatedUid)
305	{
306	  SOGoUser *delegatedUser;
307	  delegatedUser = [SOGoUser userWithLogin: delegatedUid];
308	  [delegatedAttendee setCn: [delegatedUser cn]];
309	}
310
311      [delegatedAttendee setRole: @"REQ-PARTICIPANT"];
312      [delegatedAttendee setRsvp: @"TRUE"];
313      [delegatedAttendee setParticipationStatus: iCalPersonPartStatNeedsAction];
314      [delegatedAttendee setDelegatedFrom:
315	       [NSString stringWithFormat: @"mailto:%@", [[user allEmails] objectAtIndex: 0]]];
316
317//      receiveUpdates = [[content objectForKey: @"receiveUpdates"] boolValue];
318//      if (receiveUpdates)
319//	[delegatedAttendee setRole: @"NON-PARTICIPANT"];
320
321      response = [self _changePartStatusAction: @"DELEGATED"
322				  withDelegate: delegatedAttendee];
323    }
324  else
325    response = [NSException exceptionWithHTTPStatus: 400
326					     reason: @"missing 'to' parameter"];
327
328  return response;
329}
330
331- (WOResponse *) addToCalendarAction
332{
333  iCalEvent *emailEvent;
334  SOGoAppointmentObject *eventObject;
335  SOGoUser *user;
336  WOResponse *response;
337
338  emailEvent = [self _emailEvent];
339  if (emailEvent)
340    {
341      eventObject = [self _eventObjectWithUID: [emailEvent uid]];
342      if ([eventObject isNew])
343	{
344          user = [context activeUser];
345          if (![emailEvent userIsOrganizer: user] && ![emailEvent userIsAttendee: user])
346            {
347              [emailEvent setOrganizer: nil];
348              [emailEvent removeAllAttendees];
349            }
350	  [eventObject saveCalendar: [emailEvent parent]];
351	  response = [self responseWith204];
352	}
353      else
354	{
355	  response = [self responseWithStatus: 409];
356	}
357    }
358  else
359    {
360      response = [context response];
361      [response setStatus: 409];
362      response = [self responseWithStatus: 404];
363    }
364
365  return response;
366}
367
368- (WOResponse *) deleteFromCalendarAction
369{
370  iCalEvent *emailEvent;
371  SOGoAppointmentObject *eventObject;
372  WOResponse *response;
373
374  emailEvent = [self _emailEvent];
375  if (emailEvent)
376    {
377      eventObject = [self _eventObjectWithUID: [emailEvent uid]];
378      [eventObject delete];
379      response = [self responseWith204];
380    }
381  else
382    {
383      response = [context response];
384      [response setStatus: 409];
385    }
386
387  return response;
388}
389
390- (iCalPerson *) _emailParticipantWithEvent: (iCalEvent *) event
391{
392  NGImap4EnvelopeAddress *address;
393  SOGoMailObject *mailObject;
394  NSString *emailFrom;
395  iCalPerson *p;
396
397  mailObject = [[self clientObject] mailObject];
398  address = [[mailObject fromEnvelopeAddresses] objectAtIndex: 0];
399  emailFrom = [address baseEMail];
400  p = [event findAttendeeWithEmail: emailFrom];
401
402  // We haven't found it yet, let's look in the identities
403  // associated to this user
404  if (!p)
405    {
406      SOGoUserManager *sm;
407      NSString *uid;
408
409      sm = [SOGoUserManager sharedUserManager];
410      uid = [sm getUIDForEmail: emailFrom];
411
412      if (uid)
413	{
414	  NSArray *allEmails;
415	  NSString *email;
416	  SOGoUser *u;
417	  int i;
418
419	  u = [SOGoUser userWithLogin: uid];
420	  allEmails = [u allEmails];
421	  for (i = 0; i < [allEmails count]; i++)
422	    {
423	      email = [allEmails objectAtIndex: i];
424	      if ([email caseInsensitiveCompare: emailFrom] == NSOrderedSame)
425		continue;
426
427	      p = [event findAttendeeWithEmail: email];
428
429	      if (p)
430		break;
431	    }
432	}
433    }
434
435  return p;
436}
437
438- (BOOL) _updateParticipantStatusInEvent: (iCalEvent *) calendarEvent
439			       fromEvent: (iCalEvent *) emailEvent
440				inObject: (SOGoAppointmentObject *) eventObject
441{
442  iCalPerson *calendarParticipant, *mailParticipant;
443  NSString *partStat;
444  BOOL result;
445
446  calendarParticipant = [self _emailParticipantWithEvent: calendarEvent];
447  mailParticipant = [self _emailParticipantWithEvent: emailEvent];
448  if (calendarParticipant && mailParticipant)
449    {
450      result = YES;
451      partStat = [mailParticipant partStat];
452      if ([partStat caseInsensitiveCompare: [calendarParticipant partStat]]
453	  != NSOrderedSame)
454	{
455	  [calendarParticipant setPartStat: [partStat uppercaseString]];
456	  [eventObject saveComponent: calendarEvent];
457	}
458    }
459  else
460    result = NO;
461
462  return result;
463}
464
465- (WOResponse *) updateUserStatusAction
466{
467  iCalEvent *emailEvent, *calendarEvent;
468  SOGoAppointmentObject *eventObject;
469  WOResponse *response;
470
471  response = nil;
472
473  emailEvent = [self _emailEvent];
474  if (emailEvent)
475    {
476      eventObject = [self _eventObjectWithUID: [emailEvent uid]];
477      calendarEvent = [eventObject component: NO secure: NO];
478      if (calendarEvent)
479        {
480          if (([[emailEvent sequence] compare: [calendarEvent sequence]]
481               != NSOrderedAscending)
482              && ([self _updateParticipantStatusInEvent: calendarEvent
483                                              fromEvent: emailEvent
484                                               inObject: eventObject]))
485            response = [self responseWith204];
486        }
487      else
488        response = [self responseWithStatus: 404
489                                  andString: @"Local event not found."];
490    }
491
492  if (!response)
493    {
494      response = [context response];
495      [response setStatus: 409];
496    }
497
498  return response;
499}
500
501@end
502