]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoTaskObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1008 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / Appointments / SOGoTaskObject.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import "SOGoTaskObject.h"
23
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalToDo.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
28 #import <SOGo/AgenorUserManager.h>
29 #import <NGMime/NGMime.h>
30 #import <NGMail/NGMail.h>
31 #import <NGMail/NGSendMail.h>
32 #import "SOGoAptMailNotification.h"
33 #import "common.h"
34
35 #import "NSArray+Appointments.h"
36
37 @interface SOGoTaskObject (PrivateAPI)
38 - (NSString *)homePageURLForPerson:(iCalPerson *)_person;
39   
40 - (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
41   forOldTask:(iCalToDo *)_newApt
42   andNewTask:(iCalToDo *)_oldApt
43   toAttendees:(NSArray *)_attendees;
44
45 - (void)sendInvitationEMailForTask:(iCalToDo *)_task
46   toAttendees:(NSArray *)_attendees;
47 - (void)sendTaskUpdateEMailForOldTask:(iCalToDo *)_oldApt
48   newTask:(iCalToDo *)_newApt
49   toAttendees:(NSArray *)_attendees;
50 - (void)sendAttendeeRemovalEMailForTask:(iCalToDo *)_task
51   toAttendees:(NSArray *)_attendees;
52 - (void)sendTaskDeletionEMailForTask:(iCalToDo *)_task
53   toAttendees:(NSArray *)_attendees;
54 @end
55
56 @implementation SOGoTaskObject
57
58 static NSString                  *mailTemplateDefaultLanguage = nil;
59
60 + (void)initialize {
61   NSUserDefaults      *ud;
62   static BOOL         didInit = NO;
63   
64   if (didInit) return;
65   didInit = YES;
66   
67   ud = [NSUserDefaults standardUserDefaults];
68   mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"]
69                                      retain];
70   if (!mailTemplateDefaultLanguage)
71     mailTemplateDefaultLanguage = @"French";
72 }
73
74 /* accessors */
75
76 - (iCalToDo *) task
77 {
78   return [self firstTaskFromCalendar: [self calendar]];
79 }
80
81 /* iCal handling */
82
83 - (NSArray *)attendeeUIDsFromTask:(iCalToDo *)_task {
84   AgenorUserManager *um;
85   NSMutableArray    *uids;
86   NSArray  *attendees;
87   unsigned i, count;
88   NSString *email, *uid;
89   
90   if (![_task isNotNull])
91     return nil;
92   
93   if ((attendees = [_task attendees]) == nil)
94     return nil;
95   count = [attendees count];
96   uids = [NSMutableArray arrayWithCapacity:count + 1];
97   
98   um = [AgenorUserManager sharedUserManager];
99   
100   /* add organizer */
101   
102   email = [[_task organizer] rfc822Email];
103   if ([email isNotNull]) {
104     uid = [um getUIDForEmail:email];
105     if ([uid isNotNull]) {
106       [uids addObject:uid];
107     }
108     else
109       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
110   }
111
112   /* add attendees */
113   
114   for (i = 0; i < count; i++) {
115     iCalPerson *person;
116     
117     person = [attendees objectAtIndex:i];
118     email  = [person rfc822Email];
119     if (![email isNotNull]) continue;
120     
121     uid = [um getUIDForEmail:email];
122     if (![uid isNotNull]) {
123       [self logWithFormat:@"Note: got no uid for email: '%@'", email];
124       continue;
125     }
126     if (![uids containsObject:uid])
127       [uids addObject:uid];
128   }
129   
130   return uids;
131 }
132
133 /* folder management */
134
135 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
136   // TODO: what does this do? lookup the home of the organizer?
137   return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
138 }
139 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
140   return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
141 }
142
143 /* store in all the other folders */
144
145 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
146   NSEnumerator *e;
147   id           folder;
148   NSException  *allErrors = nil;
149   id ctx;
150
151   ctx = [[WOApplication application] context];
152   
153   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
154              objectEnumerator];
155   while ((folder = [e nextObject]) != nil) {
156     NSException           *error;
157     SOGoTaskObject *task;
158     
159     if (![folder isNotNull]) /* no folder was found for given UID */
160       continue;
161     
162     task = [folder lookupName:[self nameInContainer] inContext:ctx
163                   acquire:NO];
164     if ([task isKindOfClass: [NSException class]])
165       {
166         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
167               [self nameInContainer], folder];
168         [self logWithFormat:@"the exception reason was: %@",
169               [(NSException *) task reason]];
170         continue;
171       }
172
173     if (![task isNotNull]) {
174       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
175               [self nameInContainer], folder];
176       continue;
177     }
178     
179     if ((error = [task primarySaveContentString:_iCal]) != nil) {
180       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
181       // TODO: make compound
182       allErrors = error;
183     }
184   }
185   return allErrors;
186 }
187 - (NSException *)deleteInUIDs:(NSArray *)_uids {
188   NSEnumerator *e;
189   id           folder;
190   NSException  *allErrors = nil;
191   id           ctx;
192   
193   ctx = [[WOApplication application] context];
194   
195   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
196              objectEnumerator];
197   while ((folder = [e nextObject])) {
198     NSException           *error;
199     SOGoTaskObject *task;
200     
201     task = [folder lookupName:[self nameInContainer] inContext:ctx
202                    acquire:NO];
203     if (![task isNotNull]) {
204       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
205               [self nameInContainer], folder];
206       continue;
207     }
208     if ([task isKindOfClass: [NSException class]]) {
209       [self logWithFormat:@"Exception: %@", [(NSException *) task reason]];
210       continue;
211     }
212     
213     if ((error = [task primaryDelete]) != nil) {
214       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
215       // TODO: make compound
216       allErrors = error;
217     }
218   }
219   return allErrors;
220 }
221
222 - (iCalToDo *) firstTaskFromCalendar: (iCalCalendar *) aCalendar
223 {
224   iCalToDo *task;
225   NSArray *tasks;
226
227   tasks = [aCalendar childrenWithTag: @"vtodo"];
228   if ([tasks count])
229     task = (iCalToDo *) [[tasks objectAtIndex: 0]
230                            groupWithClass: [iCalToDo class]];
231   else
232     task = nil;
233
234   return task;
235 }
236
237 /* "iCal multifolder saves" */
238
239 - (NSException *) saveContentString: (NSString *) _iCal
240                        baseSequence: (int) _v
241 {
242   /* 
243      Note: we need to delete in all participants folders and send iMIP messages
244            for all external accounts.
245      
246      Steps:
247      - fetch stored content
248      - parse old content
249      - check if sequence matches (or if 0=ignore)
250      - extract old attendee list + organizer (make unique)
251      - parse new content (ensure that sequence is increased!)
252      - extract new attendee list + organizer (make unique)
253      - make a diff => new, same, removed
254      - write to new, same
255      - delete in removed folders
256      - send iMIP mail for all folders not found
257   */
258 //   AgenorUserManager *um;
259 //   iCalCalendar *calendar;
260 //   iCalToDo *oldApt, *newApt;
261 // //   iCalToDoChanges  *changes;
262 //   iCalPerson        *organizer;
263 //   NSString          *oldContent, *uid;
264 //   NSArray           *uids, *props;
265 //   NSMutableArray    *attendees, *storeUIDs, *removedUIDs;
266   NSException       *storeError, *delError;
267 //   BOOL              updateForcesReconsider;
268   
269 //   updateForcesReconsider = NO;
270
271 //   if ([_iCal length] == 0) {
272 //     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
273 //                      reason:@"got no iCalendar content to store!"];
274 //   }
275
276 //   um = [AgenorUserManager sharedUserManager];
277
278 //   /* handle old content */
279   
280 //   oldContent = [self iCalString]; /* if nil, this is a new task */
281 //   if ([oldContent length] == 0)
282 //     {
283 //     /* new task */
284 //       [self debugWithFormat:@"saving new task: %@", _iCal];
285 //       oldApt = nil;
286 //     }
287 //   else
288 //     {
289 //       calendar = [iCalCalendar parseSingleFromSource: oldContent];
290 //       oldApt = [self firstTaskFromCalendar: calendar];
291 //     }
292   
293 //   /* compare sequence if requested */
294
295 //   if (_v != 0) {
296 //     // TODO
297 //   }
298   
299   
300 //   /* handle new content */
301   
302 //   calendar = [iCalCalendar parseSingleFromSource: _iCal];
303 //   newApt = [self firstTaskFromCalendar: calendar];
304 //   if (newApt == nil) {
305 //     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
306 //                      reason:@"could not parse iCalendar content!"];
307 //   }
308   
309 //   /* diff */
310   
311 //   changes = [iCalToDoChanges changesFromEvent: oldApt
312 //                               toEvent: newApt];
313
314 //   uids        = [um getUIDsForICalPersons:[changes deletedAttendees]
315 //                     applyStrictMapping:NO];
316 //   removedUIDs = [NSMutableArray arrayWithArray:uids];
317
318 //   uids        = [um getUIDsForICalPersons:[newApt attendees]
319 //                     applyStrictMapping:NO];
320 //   storeUIDs   = [NSMutableArray arrayWithArray:uids];
321 //   props       = [changes updatedProperties];
322
323 //   /* detect whether sequence has to be increased */
324 //   if ([changes hasChanges])
325 //     [newApt increaseSequence];
326
327 //   /* preserve organizer */
328
329 //   organizer = [newApt organizer];
330 //   uid       = [um getUIDForICalPerson:organizer];
331 //   if (uid) {
332 //     if (![storeUIDs containsObject:uid])
333 //       [storeUIDs addObject:uid];
334 //     [removedUIDs removeObject:uid];
335 //   }
336
337 //   /* organizer might have changed completely */
338
339 //   if (oldApt && ([props containsObject: @"organizer"])) {
340 //     uid = [um getUIDForICalPerson:[oldApt organizer]];
341 //     if (uid) {
342 //       if (![storeUIDs containsObject:uid]) {
343 //         if (![removedUIDs containsObject:uid]) {
344 //           [removedUIDs addObject:uid];
345 //         }
346 //       }
347 //     }
348 //   }
349
350 //   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
351 //                         storeUIDs, removedUIDs];
352
353 //   /* if time did change, all participants have to re-decide ...
354 //    * ... exception from that rule: the organizer
355 //    */
356
357 //   if (oldApt != nil &&
358 //       ([props containsObject:@"startDate"] ||
359 //        [props containsObject:@"endDate"]   ||
360 //        [props containsObject:@"duration"]))
361 //   {
362 //     NSArray  *ps;
363 //     unsigned i, count;
364     
365 //     ps    = [newApt attendees];
366 //     count = [ps count];
367 //     for (i = 0; i < count; i++) {
368 //       iCalPerson *p;
369       
370 //       p = [ps objectAtIndex:i];
371 //       if (![p hasSameEmailAddress:organizer])
372 //         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
373 //     }
374 //     _iCal = [[newApt parent] versitString];
375 //     updateForcesReconsider = YES;
376 //   }
377
378 //   /* perform storing */
379
380   storeError = [self primarySaveContentString: _iCal];
381
382 //   storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
383 //   delError   = [self deleteInUIDs:removedUIDs];
384
385   // TODO: make compound
386   if (storeError != nil) return storeError;
387 //   if (delError   != nil) return delError;
388
389   /* email notifications */
390
391 //   attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
392 //   [attendees removePerson:organizer];
393 //   [self sendInvitationEMailForTask:newApt
394 //         toAttendees:attendees];
395
396 //   if (updateForcesReconsider) {
397 //     attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
398 //     [attendees removeObjectsInArray:[changes insertedAttendees]];
399 //     [attendees removePerson:organizer];
400 //     [self sendTaskUpdateEMailForOldTask:oldApt
401 //           newTask:newApt
402 //           toAttendees:attendees];
403 //   }
404
405 //   attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
406 //   [attendees removePerson: organizer];
407 //   if ([attendees count]) {
408 //     iCalToDo *canceledApt;
409     
410 //     canceledApt = [newApt copy];
411 //     [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
412 //     [self sendAttendeeRemovalEMailForTask:canceledApt
413 //           toAttendees:attendees];
414 //     [canceledApt release];
415 //   }
416   return nil;
417 }
418
419 - (NSException *)deleteWithBaseSequence:(int)_v {
420   /* 
421      Note: We need to delete in all participants folders and send iMIP messages
422            for all external accounts.
423            Delete is basically identical to save with all attendees and the
424            organizer being deleted.
425
426      Steps:
427      - fetch stored content
428      - parse old content
429      - check if sequence matches (or if 0=ignore)
430      - extract old attendee list + organizer (make unique)
431      - delete in removed folders
432      - send iMIP mail for all folders not found
433   */
434   iCalToDo *task;
435   NSArray         *removedUIDs;
436   NSMutableArray  *attendees;
437
438   /* load existing content */
439   
440   task = [self task];
441   
442   /* compare sequence if requested */
443
444   if (_v != 0) {
445     // TODO
446   }
447   
448   removedUIDs = [self attendeeUIDsFromTask:task];
449
450   /* send notification email to attendees excluding organizer */
451   attendees = [NSMutableArray arrayWithArray:[task attendees]];
452   [attendees removePerson:[task organizer]];
453   
454   /* flag task as being canceled */
455   [(iCalCalendar *) [task parent] setMethod: @"cancel"];
456   [task increaseSequence];
457
458   /* remove all attendees to signal complete removal */
459   [task removeAllAttendees];
460
461   /* send notification email */
462   [self sendTaskDeletionEMailForTask:task
463         toAttendees:attendees];
464
465   /* perform */
466   
467   return [self deleteInUIDs:removedUIDs];
468 }
469
470 - (NSException *)saveContentString:(NSString *)_iCalString {
471   return [self saveContentString:_iCalString baseSequence:0];
472 }
473
474 - (NSException *)changeParticipationStatus:(NSString *)_status
475   inContext:(id)_ctx
476 {
477   iCalToDo *task;
478   iCalPerson      *p;
479   NSString        *newContent;
480   NSException     *ex;
481   NSString        *myEMail;
482   
483   // TODO: do we need to use SOGoTask? (prefer iCalToDo?)
484   task = [self task];
485
486   if (task == nil) {
487     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
488                         reason:@"unable to parse task record"];
489   }
490   
491   myEMail = [[_ctx activeUser] email];
492   if ((p = [task findParticipantWithEmail:myEMail]) == nil) {
493     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
494                         reason:@"user does not participate in this "
495                                @"task"];
496   }
497   
498   [p setPartStat:_status];
499   newContent = [[task parent] versitString];
500   
501   // TODO: send iMIP reply mails?
502   
503 //   [task release]; task = nil;
504   
505   if (newContent == nil) {
506     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
507                         reason:@"Could not generate iCalendar data ..."];
508   }
509   
510   if ((ex = [self saveContentString:newContent]) != nil) {
511     // TODO: why is the exception wrapped?
512     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
513                         reason:[ex reason]];
514   }
515   
516   return nil /* means: no error */;
517 }
518
519
520 /* message type */
521
522 - (NSString *)outlookMessageClass {
523   return @"IPM.Task";
524 }
525
526 /* EMail Notifications */
527
528 - (NSString *)homePageURLForPerson:(iCalPerson *)_person {
529   static AgenorUserManager *um      = nil;
530   static NSString          *baseURL = nil;
531   NSString *uid;
532
533   if (!um) {
534     WOContext *ctx;
535     NSArray   *traversalObjects;
536
537     um = [[AgenorUserManager sharedUserManager] retain];
538
539     /* generate URL from traversal stack */
540     ctx = [[WOApplication application] context];
541     traversalObjects = [ctx objectTraversalStack];
542     if ([traversalObjects count] >= 1) {
543       baseURL = [[[traversalObjects objectAtIndex:0] baseURLInContext:ctx]
544                                                      retain];
545     }
546     else {
547       [self warnWithFormat:@"Unable to create baseURL from context!"];
548       baseURL = @"http://localhost/";
549     }
550   }
551   uid = [um getUIDForEmail:[_person rfc822Email]];
552   if (!uid) return nil;
553   return [NSString stringWithFormat:@"%@%@", baseURL, uid];
554 }
555
556 - (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
557   forOldTask:(iCalToDo *)_oldApt
558   andNewTask:(iCalToDo *)_newApt
559   toAttendees:(NSArray *)_attendees
560 {
561   NSString                *pageName;
562   iCalPerson              *organizer;
563   NSString                *cn, *sender, *iCalString;
564   NGSendMail              *sendmail;
565   WOApplication           *app;
566   unsigned                i, count;
567
568   if (![_attendees count]) return; // another job neatly done :-)
569
570   /* sender */
571
572   organizer = [_newApt organizer];
573   cn        = [organizer cnWithoutQuotes];
574   if (cn) {
575     sender = [NSString stringWithFormat:@"%@ <%@>",
576                                         cn,
577                                         [organizer rfc822Email]];
578   }
579   else {
580     sender = [organizer rfc822Email];
581   }
582
583   /* generate iCalString once */
584   iCalString = [[_newApt parent] versitString];
585   
586   /* get sendmail object */
587   sendmail   = [NGSendMail sharedSendMail];
588
589   /* get WOApplication instance */
590   app        = [WOApplication application];
591
592   /* generate dynamic message content */
593
594   count = [_attendees count];
595   for (i = 0; i < count; i++) {
596     iCalPerson              *attendee;
597     NSString                *recipient;
598     SOGoAptMailNotification *p;
599     NSString                *subject, *text, *header;
600     NGMutableHashMap        *headerMap;
601     NGMimeMessage           *msg;
602     NGMimeBodyPart          *bodyPart;
603     NGMimeMultipartBody     *body;
604     
605     attendee  = [_attendees objectAtIndex:i];
606     
607     /* construct recipient */
608     cn        = [attendee cn];
609     if (cn) {
610       recipient = [NSString stringWithFormat:@"%@ <%@>",
611                                              cn,
612                                              [attendee rfc822Email]];
613     }
614     else {
615       recipient = [attendee rfc822Email];
616     }
617
618     /* create page name */
619     // TODO: select user's default language?
620     pageName   = [NSString stringWithFormat:@"SOGoAptMail%@%@",
621                                             mailTemplateDefaultLanguage,
622                                             _pageName];
623     /* construct message content */
624     p = [app pageWithName:pageName inContext:[WOContext context]];
625     [p setNewApt: _newApt];
626     [p setOldApt: _oldApt];
627     [p setHomePageURL:[self homePageURLForPerson:attendee]];
628     [p setViewTZ: [self userTimeZone: cn]];
629     subject = [p getSubject];
630     text    = [p getBody];
631
632     /* construct message */
633     headerMap = [NGMutableHashMap hashMapWithCapacity:5];
634     
635     /* NOTE: multipart/alternative seems like the correct choice but
636      * unfortunately Thunderbird doesn't offer the rich content alternative
637      * at all. Mail.app shows the rich content alternative _only_
638      * so we'll stick with multipart/mixed for the time being.
639      */
640     [headerMap setObject:@"multipart/mixed"    forKey:@"content-type"];
641     [headerMap setObject:sender                forKey:@"From"];
642     [headerMap setObject:recipient             forKey:@"To"];
643     [headerMap setObject:[NSCalendarDate date] forKey:@"date"];
644     [headerMap setObject:subject               forKey:@"Subject"];
645     msg = [NGMimeMessage messageWithHeader:headerMap];
646     
647     /* multipart body */
648     body = [[NGMimeMultipartBody alloc] initWithPart:msg];
649     
650     /* text part */
651     headerMap = [NGMutableHashMap hashMapWithCapacity:1];
652     [headerMap setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
653     bodyPart = [NGMimeBodyPart bodyPartWithHeader:headerMap];
654     [bodyPart setBody:[text dataUsingEncoding:NSUTF8StringEncoding]];
655
656     /* attach text part to multipart body */
657     [body addBodyPart:bodyPart];
658     
659     /* calendar part */
660     header     = [NSString stringWithFormat:@"text/calendar; method=%@;"
661                                             @" charset=utf-8",
662                            [(iCalCalendar *) [_newApt parent] method]];
663     headerMap  = [NGMutableHashMap hashMapWithCapacity:1];
664     [headerMap setObject:header forKey:@"content-type"];
665     bodyPart   = [NGMimeBodyPart bodyPartWithHeader:headerMap];
666     [bodyPart setBody:[iCalString dataUsingEncoding:NSUTF8StringEncoding]];
667     
668     /* attach calendar part to multipart body */
669     [body addBodyPart:bodyPart];
670     
671     /* attach multipart body to message */
672     [msg setBody:body];
673     [body release];
674
675     /* send the damn thing */
676     [sendmail sendMimePart:msg
677               toRecipients:[NSArray arrayWithObject:[attendee rfc822Email]]
678               sender:[organizer rfc822Email]];
679   }
680 }
681
682 - (void)sendInvitationEMailForTask:(iCalToDo *)_task
683   toAttendees:(NSArray *)_attendees
684 {
685   if (![_attendees count]) return; // another job neatly done :-)
686
687   [self sendEMailUsingTemplateNamed:@"Invitation"
688         forOldTask:nil
689         andNewTask:_task
690         toAttendees:_attendees];
691 }
692
693 - (void)sendTaskUpdateEMailForOldTask:(iCalToDo *)_oldApt
694   newTask:(iCalToDo *)_newApt
695   toAttendees:(NSArray *)_attendees
696 {
697   if (![_attendees count]) return;
698   
699   [self sendEMailUsingTemplateNamed:@"Update"
700         forOldTask:_oldApt
701         andNewTask:_newApt
702         toAttendees:_attendees];
703 }
704
705 - (void) sendAttendeeRemovalEMailForTask:(iCalToDo *)_task
706                              toAttendees:(NSArray *)_attendees
707 {
708   if (![_attendees count]) return;
709
710   [self sendEMailUsingTemplateNamed:@"Removal"
711         forOldTask:nil
712         andNewTask:_task
713         toAttendees:_attendees];
714 }
715
716 - (void) sendTaskDeletionEMailForTask: (iCalToDo *) _task
717                           toAttendees: (NSArray *) _attendees
718 {
719   if (![_attendees count]) return;
720
721   [self sendEMailUsingTemplateNamed:@"Deletion"
722         forOldTask:nil
723         andNewTask:_task
724         toAttendees:_attendees];
725 }
726
727 - (NSString *) davContentType
728 {
729   return @"text/calendar";
730 }
731
732 @end /* SOGoTaskObject */