Imported Upstream version 1.2.99~20120606~SE~ff65aef~SYSYNC~2728cb4
[platform/upstream/syncevolution.git] / src / backends / evolution / EvolutionCalendarSource.cpp
1 /*
2  * Copyright (C) 2005-2009 Patrick Ohly <patrick.ohly@gmx.de>
3  * Copyright (C) 2009 Intel Corporation
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) version 3.
9  *
10  * This library 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 GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  * 02110-1301  USA
19  */
20
21 #include <memory>
22 using namespace std;
23
24 #include "config.h"
25
26 #ifdef ENABLE_ECAL
27
28 // include first, it sets HANDLE_LIBICAL_MEMORY for us
29 #include <syncevo/icalstrdup.h>
30
31 #include <syncevo/SyncContext.h>
32 #include <syncevo/SmartPtr.h>
33 #include <syncevo/Logging.h>
34
35 #include "EvolutionCalendarSource.h"
36 #include "EvolutionMemoSource.h"
37 #include "e-cal-check-timezones.h"
38
39
40 #include <boost/foreach.hpp>
41
42 #include <syncevo/declarations.h>
43 SE_BEGIN_CXX
44
45 static const string
46 EVOLUTION_CALENDAR_PRODID("PRODID:-//ACME//NONSGML SyncEvolution//EN"),
47 EVOLUTION_CALENDAR_VERSION("VERSION:2.0");
48
49 bool EvolutionCalendarSource::LUIDs::containsLUID(const ItemID &id) const
50 {
51     const_iterator it = findUID(id.m_uid);
52     return it != end() &&
53         it->second.find(id.m_rid) != it->second.end();
54 }
55
56 void EvolutionCalendarSource::LUIDs::insertLUID(const ItemID &id)
57 {
58     (*this)[id.m_uid].insert(id.m_rid);
59 }
60
61 void EvolutionCalendarSource::LUIDs::eraseLUID(const ItemID &id)
62 {
63     iterator it = find(id.m_uid);
64     if (it != end()) {
65         set<string>::iterator it2 = it->second.find(id.m_rid);
66         if (it2 != it->second.end()) {
67             it->second.erase(it2);
68             if (it->second.empty()) {
69                 erase(it);
70             }
71         }
72     }
73 }
74
75 static int granularity()
76 {
77     // This long delay is necessary in combination
78     // with Evolution Exchange Connector: when updating
79     // a child event, it seems to take a while until
80     // the change really is effective.
81     static int secs = 5;
82     static bool checked = false;
83     if (!checked) {
84         // allow setting the delay (used during testing to shorten runtime)
85         const char *delay = getenv("SYNC_EVOLUTION_EVO_CALENDAR_DELAY");
86         if (delay) {
87             secs = atoi(delay);
88         }
89         checked = true;
90     }
91     return secs;
92 }
93
94 EvolutionCalendarSource::EvolutionCalendarSource(EvolutionCalendarSourceType type,
95                                                  const SyncSourceParams &params) :
96     EvolutionSyncSource(params, granularity()),
97     m_type(type)
98 {
99     switch (m_type) {
100      case EVOLUTION_CAL_SOURCE_TYPE_EVENTS:
101         SyncSourceLogging::init(InitList<std::string>("SUMMARY") + "LOCATION",
102                                 ", ",
103                                 m_operations);
104         m_typeName = "calendar";
105 #ifndef USE_ECAL_CLIENT
106         m_newSystem = e_cal_new_system_calendar;
107 #endif
108         break;
109      case EVOLUTION_CAL_SOURCE_TYPE_TASKS:
110         SyncSourceLogging::init(InitList<std::string>("SUMMARY"),
111                                 ", ",
112                                 m_operations);
113         m_typeName = "task list";
114 #ifndef USE_ECAL_CLIENT
115         m_newSystem = e_cal_new_system_tasks;
116 #endif
117         break;
118      case EVOLUTION_CAL_SOURCE_TYPE_MEMOS:
119         SyncSourceLogging::init(InitList<std::string>("SUBJECT"),
120                                 ", ",
121                                 m_operations);
122         m_typeName = "memo list";
123 #ifndef USE_ECAL_CLIENT
124         // This is not available in older Evolution versions.
125         // A configure check could detect that, but as this isn't
126         // important the functionality is simply disabled.
127         m_newSystem = NULL /* e_cal_new_system_memos */;
128 #endif
129         break;
130      default:
131         SyncContext::throwError("internal error, invalid calendar type");
132         break;
133     }
134 }
135
136 SyncSource::Databases EvolutionCalendarSource::getDatabases()
137 {
138     ESourceList *tmp = NULL;
139     GErrorCXX gerror;
140     Databases result;
141
142     if (
143 #ifdef USE_ECAL_CLIENT
144         !e_cal_client_get_sources(&tmp, sourceType(), gerror)
145 #else
146         !e_cal_get_sources(&tmp, sourceType(), gerror)
147 #endif
148         ) {
149         // ignore unspecific errors (like on Maemo with no support for memos)
150         // and continue with empty list (perhaps defaults work)
151         if (!gerror) {
152             tmp = NULL;
153         } else {
154             throwError("unable to access backend databases", gerror);
155         }
156     }
157     ESourceListCXX sources(tmp, false);
158
159     bool first = true;
160     for (GSList *g = sources ? e_source_list_peek_groups (sources) : NULL;
161          g;
162          g = g->next) {
163         ESourceGroup *group = E_SOURCE_GROUP (g->data);
164         for (GSList *s = e_source_group_peek_sources (group); s; s = s->next) {
165             ESource *source = E_SOURCE (s->data);
166             eptr<char> uri(e_source_get_uri(source));
167             result.push_back(Database(e_source_peek_name(source),
168                                       uri ? uri.get() : "",
169                                       first));
170             first = false;
171         }
172     }
173
174 #ifdef USE_ECAL_CLIENT
175     if (result.empty()) {
176         ECalClientCXX calendar = ECalClientCXX::steal(e_cal_client_new_system(sourceType(), NULL));
177         if (calendar) {
178           const char *uri = e_client_get_uri (E_CLIENT ((ECalClient*)calendar));
179           result.push_back(Database("<<system>>", uri ? uri : "<<unknown uri>>"));
180         }
181     }
182 #else
183     if (result.empty() && m_newSystem) {
184         eptr<ECal, GObject> calendar(m_newSystem());
185         if (calendar.get()) {
186             // okay, default system database exists
187             const char *uri = e_cal_get_uri(calendar.get());
188             result.push_back(Database("<<system>>", uri ? uri : "<<unknown uri>>"));
189         }
190     }
191 #endif
192
193     return result;
194 }
195
196 #ifdef USE_ECAL_CLIENT
197 static void
198 handle_error_cb (EClient */*client*/, const gchar *error_msg, gpointer user_data)
199 {
200     EvolutionCalendarSource *that = static_cast<EvolutionCalendarSource *>(user_data);
201     SE_LOG_ERROR(that, NULL, "%s", error_msg);
202 }
203
204 static gboolean
205 handle_authentication_cb (EClient */*client*/, ECredentials *credentials, gpointer user_data)
206 {
207     EvolutionCalendarSource *that = static_cast<EvolutionCalendarSource *>(user_data);
208     std::string passwd = that->getPassword();
209     std::string prompt = e_credentials_peek(credentials, E_CREDENTIALS_KEY_PROMPT_TEXT);
210     std::string key = e_credentials_peek(credentials, E_CREDENTIALS_KEY_PROMPT_KEY);
211
212     SE_LOG_DEBUG(that, NULL, "authentication requested, prompt \"%s\", key \"%s\" => %s",
213                  prompt.c_str(), key.c_str(),
214                  !passwd.empty() ? "returning configured password" : "no password configured");
215
216     if (!passwd.empty()) {
217         e_credentials_set (credentials, E_CREDENTIALS_KEY_PASSWORD, passwd.c_str());
218         return true;
219     } else {
220         return false;
221     }
222 }
223
224 #else
225
226 char *EvolutionCalendarSource::authenticate(const char *prompt,
227                                             const char *key)
228 {
229     std::string passwd = getPassword();
230
231     SE_LOG_DEBUG(this, NULL, "authentication requested, prompt \"%s\", key \"%s\" => %s",
232                  prompt, key,
233                  !passwd.empty() ? "returning configured password" : "no password configured");
234     return !passwd.empty() ? strdup(passwd.c_str()) : NULL;
235 }
236
237 #endif
238
239 void EvolutionCalendarSource::open()
240 {
241     ESourceList *tmp;
242     GErrorCXX gerror;
243     bool onlyIfExists = false; // always try to create address book, because even if there is
244                                // a source there's no guarantee that the actual database was
245                                // created already; the original logic below for only setting
246                                // this when explicitly requesting a new database
247                                // therefore failed in some cases
248
249 #ifdef USE_ECAL_CLIENT
250     if (!e_cal_client_get_sources (&tmp, sourceType(), gerror)) {
251         throwError("unable to access backend databases", gerror);
252     }
253     ESourceListCXX sources(tmp, false);
254
255     string id = getDatabaseID();    
256     ESource *source = findSource(sources, id);
257     bool created = false;
258
259     // Open twice. This solves an issue where Evolution's CalDAV
260     // backend only updates its local cache *after* a sync (= while
261     // closing the calendar?), instead of doing it *before* a sync (in
262     // e_cal_open()).
263     //
264     // This workaround is applied to *all* backends because there might
265     // be others with similar problems and for local storage it is
266     // a reasonably cheap operation (so no harm there).
267     for (int retries = 0; retries < 2; retries++) {
268         if (!source) {
269             // might have been special "<<system>>" or "<<default>>", try that and
270             // creating address book from file:// URI before giving up
271             if ((id.empty() || id == "<<system>>")) {
272                 m_calendar = ECalClientCXX::steal(e_cal_client_new_system(sourceType(), gerror));
273             } else if (!id.compare(0, 7, "file://")) {
274                 m_calendar = ECalClientCXX::steal(e_cal_client_new_from_uri(id.c_str(), sourceType(), gerror));
275             } else {
276                 throwError(string("not found: '") + id + "'");
277             }
278             created = true;
279         } else {
280             m_calendar = ECalClientCXX::steal(e_cal_client_new(source, sourceType(), gerror));
281         }
282
283         if (gerror) {
284             throwError("create calendar", gerror);
285         }
286
287         // Listen for errors
288         g_signal_connect (m_calendar, "backend-error", G_CALLBACK (handle_error_cb), this); 
289
290         // Handle authentication requests from the backend
291         g_signal_connect (m_calendar, "authenticate", G_CALLBACK (handle_authentication_cb), this);
292     
293         if (!e_client_open_sync(E_CLIENT ((ECalClient*)m_calendar), onlyIfExists, NULL, gerror)) {
294             if (created) {
295                 // opening newly created address books often failed, perhaps that also applies to calendars - try again
296                 gerror.clear();
297                 sleep(5);
298                 if (!e_client_open_sync(E_CLIENT ((ECalClient*)m_calendar), onlyIfExists, NULL, gerror)) {
299                     throwError(string("opening ") + m_typeName , gerror);
300                 }
301             } else {
302                 throwError(string("opening ") + m_typeName , gerror);
303             }
304         }
305     }
306 #else
307     if (!e_cal_get_sources(&tmp, sourceType(), gerror)) {
308         throwError("unable to access backend databases", gerror);
309     }
310     ESourceListCXX sources(tmp, false);
311
312     string id = getDatabaseID();    
313     ESource *source = findSource(sources, id);
314     bool created = false;
315
316     // Open twice. This solves an issue where Evolution's CalDAV
317     // backend only updates its local cache *after* a sync (= while
318     // closing the calendar?), instead of doing it *before* a sync (in
319     // e_cal_open()).
320     //
321     // This workaround is applied to *all* backends because there might
322     // be others with similar problems and for local storage it is
323     // a reasonably cheap operation (so no harm there).
324     for (int retries = 0; retries < 2; retries++) {
325         if (!source) {
326             // might have been special "<<system>>" or "<<default>>", try that and
327             // creating address book from file:// URI before giving up
328             if ((id.empty() || id == "<<system>>") && m_newSystem) {
329                 m_calendar.set(m_newSystem(), (string("system ") + m_typeName).c_str());
330             } else if (!id.compare(0, 7, "file://")) {
331                 m_calendar.set(e_cal_new_from_uri(id.c_str(), sourceType()), (string("creating ") + m_typeName).c_str());
332             } else {
333                 throwError(string("not found: '") + id + "'");
334             }
335             created = true;
336             onlyIfExists = false;
337         } else {
338             m_calendar.set(e_cal_new(source, sourceType()), m_typeName.c_str());
339         }
340
341         e_cal_set_auth_func(m_calendar, eCalAuthFunc, this);
342     
343         if (!e_cal_open(m_calendar, onlyIfExists, gerror)) {
344             if (created) {
345                 // opening newly created address books often failed, perhaps that also applies to calendars - try again
346                 gerror.clear();
347                 sleep(5);
348                 if (!e_cal_open(m_calendar, onlyIfExists, gerror)) {
349                     throwError(string("opening ") + m_typeName, gerror);
350                 }
351             } else {
352                 throwError(string("opening ") + m_typeName, gerror);
353             }
354         }
355     }
356
357 #endif
358
359     g_signal_connect_after(m_calendar,
360                            "backend-died",
361                            G_CALLBACK(SyncContext::fatalError),
362                            (void *)"Evolution Data Server has died unexpectedly, database no longer available.");
363 }
364
365 bool EvolutionCalendarSource::isEmpty()
366 {
367     // TODO: add more efficient implementation which does not
368     // depend on actually pulling all items from EDS
369     RevisionMap_t revisions;
370     listAllItems(revisions);
371     return revisions.empty();
372 }
373
374 #ifdef USE_ECAL_CLIENT
375 class ECalClientViewSyncHandler {
376   public:
377     ECalClientViewSyncHandler(ECalClientView *view, void (*processList)(const GSList *list, void *user_data), void *user_data): 
378         m_processList(processList), m_userData(user_data), m_view(view)
379     {}
380
381     bool processSync(GErrorCXX &gerror)
382     {
383         // Listen for view signals
384         g_signal_connect(m_view, "objects-added", G_CALLBACK(objectsAdded), this);
385         g_signal_connect(m_view, "complete", G_CALLBACK(completed), this);
386
387         // Start the view
388         e_cal_client_view_start (m_view, m_error);
389         if (m_error) {
390             std::swap(gerror, m_error);
391             return false;
392         }
393
394         // Async -> Sync
395         m_loop.run();
396         e_cal_client_view_stop (m_view, NULL);
397
398         if (m_error) {
399             std::swap(gerror, m_error);
400             return false;
401         } else {
402             return true;
403         }
404     }
405  
406     static void objectsAdded(ECalClientView *ebookview,
407                              const GSList *objects,
408                              gpointer user_data) {
409         ECalClientViewSyncHandler *that = (ECalClientViewSyncHandler *)user_data;
410         that->m_processList(objects, that->m_userData);
411     }
412  
413     static void completed(ECalClientView *ebookview,
414                           const GError *error,
415                           gpointer user_data) {
416         ECalClientViewSyncHandler *that = (ECalClientViewSyncHandler *)user_data;
417         that->m_error = error;
418         that->m_loop.quit();
419     }
420  
421     public:
422       // Process list callback
423       void (*m_processList)(const GSList *list, void *user_data);
424       void *m_userData;
425       // Event loop for Async -> Sync
426       EvolutionAsync m_loop;
427
428     private:
429       // View watched
430       ECalClientView *m_view;
431
432       // Possible error while watching the view
433       GErrorCXX m_error;
434 };
435
436 static void list_revisions(const GSList *objects, void *user_data)
437 {
438     EvolutionCalendarSource::RevisionMap_t *revisions = 
439         static_cast<EvolutionCalendarSource::RevisionMap_t *>(user_data);
440     const GSList *l;
441
442     for (l = objects; l; l = l->next) {
443         icalcomponent *icomp = (icalcomponent*)l->data;
444         EvolutionCalendarSource::ItemID id = EvolutionCalendarSource::getItemID(icomp);
445         string luid = id.getLUID();
446         string modTime = EvolutionCalendarSource::getItemModTime(icomp);
447
448         (*revisions)[luid] = modTime;
449     }
450 }
451 #endif
452
453 void EvolutionCalendarSource::listAllItems(RevisionMap_t &revisions)
454 {
455     GErrorCXX gerror;
456 #ifdef USE_ECAL_CLIENT
457     ECalClientView *view;
458
459     if (!e_cal_client_get_view_sync (m_calendar, "#t", &view, NULL, gerror)) {
460         throwError( "getting the view" , gerror);
461     }
462     ECalClientViewCXX viewPtr = ECalClientViewCXX::steal(view);
463
464     // TODO: Optimization: use set fields_of_interest (UID / REV / LAST-MODIFIED)
465
466     ECalClientViewSyncHandler handler(viewPtr, list_revisions, &revisions);
467     if (!handler.processSync(gerror)) {
468         throwError("watching view", gerror);
469     }
470
471     // Update m_allLUIDs
472     m_allLUIDs.clear();
473     RevisionMap_t::iterator it;
474     for(it = revisions.begin(); it != revisions.end(); it++) {
475         m_allLUIDs.insertLUID(it->first);
476     }
477 #else
478     GList *nextItem;
479
480     m_allLUIDs.clear();
481     if (!e_cal_get_object_list_as_comp(m_calendar,
482                                        "#t",
483                                        &nextItem,
484                                        gerror)) {
485         throwError("reading all items", gerror);
486     }
487     eptr<GList> listptr(nextItem);
488     while (nextItem) {
489         ECalComponent *ecomp = E_CAL_COMPONENT(nextItem->data);
490         ItemID id = getItemID(ecomp);
491         string luid = id.getLUID();
492         string modTime = getItemModTime(ecomp);
493
494         m_allLUIDs.insertLUID(id);
495         revisions[luid] = modTime;
496         nextItem = nextItem->next;
497     }
498 #endif
499 }
500
501 void EvolutionCalendarSource::close()
502 {
503     m_calendar = NULL;
504 }
505
506 void EvolutionCalendarSource::readItem(const string &luid, std::string &item, bool raw)
507 {
508     ItemID id(luid);
509     item = retrieveItemAsString(id);
510 }
511
512 #ifdef USE_ECAL_CLIENT
513 icaltimezone *
514 my_tzlookup(const gchar *tzid,
515             gconstpointer ecalclient,
516             GCancellable *cancellable,
517             GError **error)
518 {
519     icaltimezone *zone = NULL;
520     GError *local_error = NULL;
521
522     if (e_cal_client_get_timezone_sync((ECalClient *)ecalclient, tzid, &zone, cancellable, &local_error)) {
523         return zone;
524     } else if (local_error && local_error->domain == E_CAL_CLIENT_ERROR) {
525         // Ignore *all* E_CAL_CLIENT_ERROR errors, e_cal_client_get_timezone_sync() does
526         // not reliably return a specific code like E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND.
527         // See the 'e_cal_client_check_timezones() + e_cal_client_tzlookup() + Could not retrieve calendar time zone: Invalid object'
528         // mail thread.
529         g_clear_error (&local_error);
530     } else if (local_error) {
531         g_propagate_error (error, local_error);
532     }
533
534     return NULL;
535 }
536 #endif
537
538 EvolutionCalendarSource::InsertItemResult EvolutionCalendarSource::insertItem(const string &luid, const std::string &item, bool raw)
539 {
540     bool update = !luid.empty();
541     InsertItemResultState state = ITEM_OKAY;
542     bool detached = false;
543     string newluid = luid;
544     string data = item;
545     string modTime;
546
547     /*
548      * Evolution/libical can only deal with \, as separator.
549      * Replace plain , in incoming event CATEGORIES with \, -
550      * based on simple text search/replace and thus will not work
551      * in all cases...
552      *
553      * Inverse operation in extractItemAsString().
554      */
555     size_t propstart = data.find("\nCATEGORIES");
556     bool modified = false;
557     while (propstart != data.npos) {
558         size_t eol = data.find('\n', propstart + 1);
559         size_t comma = data.find(',', propstart);
560
561         while (eol != data.npos &&
562                comma != data.npos &&
563                comma < eol) {
564             if (data[comma-1] != '\\') {
565                 data.insert(comma, "\\");
566                 comma++;
567                 modified = true;
568             }
569             comma = data.find(',', comma + 1);
570         }
571         propstart = data.find("\nCATEGORIES", propstart + 1);
572     }
573     if (modified) {
574         SE_LOG_DEBUG(this, NULL, "after replacing , with \\, in CATEGORIES:\n%s", data.c_str());
575     }
576
577     eptr<icalcomponent> icomp(icalcomponent_new_from_string((char *)data.c_str()));
578
579     if( !icomp ) {
580         throwError(string("failure parsing ical") + data);
581     }
582
583     GErrorCXX gerror;
584
585     // fix up TZIDs
586     if (
587 #ifdef USE_ECAL_CLIENT
588         !e_cal_client_check_timezones(icomp,
589                                       NULL,
590                                       my_tzlookup,
591                                       (const void *)m_calendar.get(),
592                                       NULL,
593                                       gerror)
594 #else
595         !e_cal_check_timezones(icomp,
596                                NULL,
597                                e_cal_tzlookup_ecal,
598                                (const void *)m_calendar.get(),
599                                gerror)
600 #endif
601         ) {
602         throwError(string("fixing timezones") + data,
603                    gerror);
604     }
605
606     // insert before adding/updating the event so that the new VTIMEZONE is
607     // immediately available should anyone want it
608     for (icalcomponent *tcomp = icalcomponent_get_first_component(icomp, ICAL_VTIMEZONE_COMPONENT);
609          tcomp;
610          tcomp = icalcomponent_get_next_component(icomp, ICAL_VTIMEZONE_COMPONENT)) {
611         eptr<icaltimezone> zone(icaltimezone_new(), "icaltimezone");
612         icaltimezone_set_component(zone, tcomp);
613
614         GErrorCXX gerror;
615         const char *tzid = icaltimezone_get_tzid(zone);
616         if (!tzid || !tzid[0]) {
617             // cannot add a VTIMEZONE without TZID
618             SE_LOG_DEBUG(this, NULL, "skipping VTIMEZONE without TZID");
619         } else {
620             gboolean success =
621 #ifdef USE_ECAL_CLIENT
622                 e_cal_client_add_timezone_sync(m_calendar, zone, NULL, gerror)
623 #else
624                 e_cal_add_timezone(m_calendar, zone, gerror)
625 #endif
626                 ;
627             if (!success) {
628                 throwError(string("error adding VTIMEZONE ") + tzid,
629                            gerror);
630             }
631         }
632     }
633
634     // the component to update/add must be the
635     // ICAL_VEVENT/VTODO_COMPONENT of the item,
636     // e_cal_create/modify_object() fail otherwise
637     icalcomponent *subcomp = icalcomponent_get_first_component(icomp,
638                                                                getCompType());
639     if (!subcomp) {
640         throwError("extracting event");
641     }
642
643     // Remove LAST-MODIFIED: the Evolution Exchange Connector does not
644     // properly update this property if it is already present in the
645     // incoming data.
646     icalproperty *modprop;
647     while ((modprop = icalcomponent_get_first_property(subcomp, ICAL_LASTMODIFIED_PROPERTY)) != NULL) {
648         icalcomponent_remove_property(subcomp, modprop);
649         icalproperty_free(modprop);
650         modprop = NULL;
651     }
652
653     if (!update) {
654         ItemID id = getItemID(subcomp);
655         const char *uid = NULL;
656
657         // Trying to add a normal event which already exists leads to a
658         // gerror->domain == E_CALENDAR_ERROR
659         // gerror->code == E_CALENDAR_STATUS_OBJECT_ID_ALREADY_EXISTS
660         // error. Depending on the Evolution version, the subcomp
661         // UID gets removed (>= 2.12) or remains unchanged.
662         //
663         // Existing detached recurrences are silently updated when
664         // trying to add them. This breaks our return code and change
665         // tracking.
666         //
667         // Escape this madness by checking the existence ourselve first
668         // based on our list of existing LUIDs. Note that this list is
669         // not updated during a sync. This is correct as long as no LUID
670         // gets used twice during a sync (examples: add + add, delete + add),
671         // which should never happen.
672         newluid = id.getLUID();
673         if (m_allLUIDs.containsLUID(id)) {
674             state = ITEM_NEEDS_MERGE;
675         } else {
676             // if this is a detached recurrence, then we
677             // must use e_cal_modify_object() below if
678             // the parent or any other child already exists
679             if (!id.m_rid.empty() &&
680                 m_allLUIDs.containsUID(id.m_uid)) {
681                 detached = true;
682             } else {
683                 // Creating the parent while children are already in
684                 // the calendar confuses EDS (at least 2.12): the
685                 // parent is stored in the .ics with the old UID, but
686                 // the uid returned to the caller is a different
687                 // one. Retrieving the item then fails. Avoid this
688                 // problem by removing the children from the calendar,
689                 // adding the parent, then updating it with the
690                 // saved children.
691                 //
692                 // TODO: still necessary with e_cal_client API?
693                 ICalComps_t children;
694                 if (id.m_rid.empty()) {
695                     children = removeEvents(id.m_uid, true);
696                 }
697
698                 // creating new objects works for normal events and detached occurrences alike
699                 if (
700 #ifdef USE_ECAL_CLIENT
701                     e_cal_client_create_object_sync(m_calendar, subcomp, (gchar **)&uid, 
702                                                     NULL, gerror)
703 #else
704                     e_cal_create_object(m_calendar, subcomp, (gchar **)&uid, gerror)
705 #endif
706                     ) {
707 #ifdef USE_ECAL_CLIENT
708                     PlainGStr owner((gchar *)uid);
709 #endif
710                     // Evolution workaround: don't rely on uid being set if we already had
711                     // one. In Evolution 2.12.1 it was set to garbage. The recurrence ID
712                     // shouldn't have changed either.
713                     ItemID newid(!id.m_uid.empty() ? id.m_uid : uid, id.m_rid);
714                     newluid = newid.getLUID();
715                     modTime = getItemModTime(newid);
716                     m_allLUIDs.insertLUID(newid);
717                 } else {
718                     throwError("storing new item", gerror);
719                 }
720
721                 // Recreate any children removed earlier: when we get here,
722                 // the parent exists and we must update it.
723                 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
724                     if (
725 #ifdef USE_ECAL_CLIENT
726                         !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
727                                                          CALOBJ_MOD_THIS, NULL,
728                                                          gerror)
729 #else
730                         !e_cal_modify_object(m_calendar, *icalcomp,
731                                              CALOBJ_MOD_THIS,
732                                              gerror)
733 #endif
734                         ) {
735                         throwError(string("recreating item ") + newluid, gerror);
736                     }
737                 }
738             }
739         }
740     }
741
742     if (update ||
743         (state != ITEM_OKAY && state != ITEM_NEEDS_MERGE) ||
744         detached) {
745         ItemID id(newluid);
746         bool isParent = id.m_rid.empty();
747
748         // ensure that the component has the right UID and
749         // RECURRENCE-ID
750         if (update) {
751             if (!id.m_uid.empty()) {
752                 icalcomponent_set_uid(subcomp, id.m_uid.c_str());
753             }
754             if (!id.m_rid.empty()) {
755                 // Reconstructing the RECURRENCE-ID is non-trivial,
756                 // because our luid only contains the date-time, but
757                 // not the time zone. Only do the work if the event
758                 // really doesn't have a RECURRENCE-ID.
759                 struct icaltimetype rid;
760                 rid = icalcomponent_get_recurrenceid(subcomp);
761                 if (icaltime_is_null_time(rid)) {
762                     // Preserve the original RECURRENCE-ID, including
763                     // timezone, no matter what the update contains
764                     // (might have wrong timezone or UTC).
765                     eptr<icalcomponent> orig(retrieveItem(id));
766                     icalproperty *orig_rid = icalcomponent_get_first_property(orig, ICAL_RECURRENCEID_PROPERTY);
767                     if (orig_rid) {
768                         icalcomponent_add_property(subcomp, icalproperty_new_clone(orig_rid));
769                     }
770                 }
771             }
772         }
773
774         if (isParent) {
775             // CALOBJ_MOD_THIS for parent items (UID set, no RECURRENCE-ID)
776             // is not supported by all backends: the Exchange Connector
777             // fails with it. It might be an incorrect usage of the API.
778             // Therefore we have to use CALOBJ_MOD_ALL, but that removes
779             // children.
780             bool hasChildren = false;
781             LUIDs::const_iterator it = m_allLUIDs.find(id.m_uid);
782             if (it != m_allLUIDs.end()) {
783                 BOOST_FOREACH(const string &rid, it->second) {
784                     if (!rid.empty()) {
785                         hasChildren = true;
786                         break;
787                     }
788                 }
789             }
790
791             if (hasChildren) {
792                 // Use CALOBJ_MOD_ALL and temporarily remove
793                 // the children, then add them again. Otherwise they would
794                 // get deleted.
795                 ICalComps_t children = removeEvents(id.m_uid, true);
796
797                 // Parent is gone, too, and needs to be recreated.
798                 const char *uid = NULL;
799                 if (
800 #ifdef USE_ECAL_CLIENT
801                     !e_cal_client_create_object_sync(m_calendar, subcomp, (char **)&uid, 
802                                                      NULL, gerror)
803 #else
804                     !e_cal_create_object(m_calendar, subcomp, (char **)&uid, gerror)
805 #endif
806                     ) {
807                     throwError(string("creating updated item ") + luid, gerror);
808                 }
809 #ifdef USE_ECAL_CLIENT
810                 PlainGStr owner((gchar *)uid);
811 #endif
812
813                 // Recreate any children removed earlier: when we get here,
814                 // the parent exists and we must update it.
815                 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
816                     if (
817 #ifdef USE_ECAL_CLIENT
818                         !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
819                                                          CALOBJ_MOD_THIS, NULL,
820                                                          gerror)
821 #else
822                         !e_cal_modify_object(m_calendar, *icalcomp,
823                                              CALOBJ_MOD_THIS,
824                                              gerror)
825 #endif
826                         ) {
827                         throwError(string("recreating item ") + luid, gerror);
828                     }
829                 }
830             } else {
831                 // no children, updating is simple
832                 if (
833 #ifdef USE_ECAL_CLIENT
834                     !e_cal_client_modify_object_sync(m_calendar, subcomp,
835                                                      CALOBJ_MOD_ALL, NULL,
836                                                      gerror)
837 #else
838                     !e_cal_modify_object(m_calendar, subcomp,
839                                          CALOBJ_MOD_ALL,
840                                          gerror)
841 #endif
842                     ) {
843                     throwError(string("updating item ") + luid, gerror);
844                 }
845             }
846         } else {
847             // child event
848             if (
849 #ifdef USE_ECAL_CLIENT
850                 !e_cal_client_modify_object_sync(m_calendar, subcomp,
851                                                  CALOBJ_MOD_THIS, NULL,
852                                                  gerror)
853 #else
854                 !e_cal_modify_object(m_calendar, subcomp,
855                                      CALOBJ_MOD_THIS,
856                                      gerror)
857 #endif
858                 ) {
859                 throwError(string("updating item ") + luid, gerror);
860             }
861         }
862
863         ItemID newid = getItemID(subcomp);
864         newluid = newid.getLUID();
865         modTime = getItemModTime(newid);
866     }
867
868     return InsertItemResult(newluid, modTime, state);
869 }
870
871 EvolutionCalendarSource::ICalComps_t EvolutionCalendarSource::removeEvents(const string &uid, bool returnOnlyChildren, bool ignoreNotFound)
872 {
873     ICalComps_t events;
874
875     LUIDs::const_iterator it = m_allLUIDs.find(uid);
876     if (it != m_allLUIDs.end()) {
877         BOOST_FOREACH(const string &rid, it->second) {
878             ItemID id(uid, rid);
879             icalcomponent *icomp = retrieveItem(id);
880             if (icomp) {
881                 if (id.m_rid.empty() && returnOnlyChildren) {
882                     icalcomponent_free(icomp);
883                 } else {
884                     events.push_back(ICalComps_t::value_type(new eptr<icalcomponent>(icomp)));
885                 }
886             }
887         }
888     }
889
890     // removes all events with that UID, including children
891     GErrorCXX gerror;
892     if (
893 #ifdef USE_ECAL_CLIENT
894         !e_cal_client_remove_object_sync(m_calendar,
895                                          uid.c_str(), NULL, CALOBJ_MOD_ALL,
896                                          NULL, gerror)
897
898 #else
899         !e_cal_remove_object(m_calendar,
900                              uid.c_str(),
901                              gerror)
902 #endif
903         ) {
904         if (IsCalObjNotFound(gerror)) {
905             SE_LOG_DEBUG(this, NULL, "%s: request to delete non-existant item ignored",
906                          uid.c_str());
907             if (!ignoreNotFound) {
908                 throwError(STATUS_NOT_FOUND, string("delete item: ") + uid);
909             }
910         } else {
911             throwError(string("deleting item " ) + uid, gerror);
912         }
913     }
914
915     return events;
916 }
917
918 void EvolutionCalendarSource::removeItem(const string &luid)
919 {
920     GErrorCXX gerror;
921     ItemID id(luid);
922
923     if (id.m_rid.empty()) {
924         /*
925          * Removing the parent item also removes all children. Evolution
926          * does that automatically. Calling e_cal_remove_object_with_mod()
927          * without valid rid confuses Evolution, don't do it. As a workaround
928          * remove all items with the given uid and if we only wanted to
929          * delete the parent, then recreate the children.
930          */
931         ICalComps_t children = removeEvents(id.m_uid, true, false);
932
933         // recreate children
934         bool first = true;
935         BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
936             if (first) {
937                 char *uid;
938
939                 if (
940 #ifdef USE_ECAL_CLIENT
941                     !e_cal_client_create_object_sync(m_calendar, *icalcomp, &uid, 
942                                                      NULL, gerror)
943 #else
944                     !e_cal_create_object(m_calendar, *icalcomp, &uid, gerror)
945 #endif
946                     ) {
947                     throwError(string("recreating first item ") + luid, gerror);
948                 }
949 #ifdef USE_ECAL_CLIENT
950                 PlainGStr owner((gchar *)uid);
951 #endif
952                 first = false;
953             } else {
954                 if (
955 #ifdef USE_ECAL_CLIENT
956                     !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
957                                                      CALOBJ_MOD_THIS, NULL,
958                                                      gerror)
959 #else
960                     !e_cal_modify_object(m_calendar, *icalcomp,
961                                          CALOBJ_MOD_THIS,
962                                          gerror)
963 #endif
964                     ) {
965                     throwError(string("recreating following item ") + luid, gerror);
966                 }
967             }
968         }
969     } else {
970         // workaround for EDS 2.32 API semantic: succeeds even if
971         // detached recurrence doesn't exist and adds EXDATE,
972         // therefore we have to check for existence first
973         eptr<icalcomponent> item(retrieveItem(id));
974         gboolean success = !item ? false :
975 #ifdef USE_ECAL_CLIENT
976             // TODO: is this necessary?
977             e_cal_client_remove_object_sync(m_calendar,
978                                             id.m_uid.c_str(),
979                                             id.m_rid.c_str(),
980                                             CALOBJ_MOD_ONLY_THIS,
981                                             NULL,
982                                             gerror)
983 #else
984             e_cal_remove_object_with_mod(m_calendar,
985                                          id.m_uid.c_str(),
986                                          id.m_rid.c_str(),
987                                          CALOBJ_MOD_THIS,
988                                          gerror)
989 #endif
990             ;
991         if (!item ||
992             (!success && IsCalObjNotFound(gerror))) {
993             SE_LOG_DEBUG(this, NULL, "%s: request to delete non-existant item",
994                          luid.c_str());
995             throwError(STATUS_NOT_FOUND, string("delete item: ") + id.getLUID());
996         } else if (!success) {
997             throwError(string("deleting item " ) + luid, gerror);
998         }
999     }
1000     m_allLUIDs.eraseLUID(id);
1001
1002     if (!id.m_rid.empty()) {
1003         // Removing the child may have modified the parent.
1004         // We must record the new LAST-MODIFIED string,
1005         // otherwise it might be reported as modified during
1006         // the next sync (timing dependent: if the parent
1007         // was updated before removing the child *and* the
1008         // update and remove fall into the same second,
1009         // then the modTime does not change again during the
1010         // removal).
1011         try {
1012             ItemID parent(id.m_uid, "");
1013             string modTime = getItemModTime(parent);
1014             string parentLUID = parent.getLUID();
1015             updateRevision(getTrackingNode(), parentLUID, parentLUID, modTime);
1016         } catch (...) {
1017             // There's no guarantee that the parent still exists.
1018             // Instead of checking that, ignore errors (a bit hacky,
1019             // but better than breaking the removal).
1020         }
1021     }
1022 }
1023
1024 icalcomponent *EvolutionCalendarSource::retrieveItem(const ItemID &id)
1025 {
1026     GErrorCXX gerror;
1027     icalcomponent *comp = NULL;
1028
1029     if (
1030 #ifdef USE_ECAL_CLIENT
1031         !e_cal_client_get_object_sync(m_calendar,
1032                                       id.m_uid.c_str(),
1033                                       !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
1034                                       &comp,
1035                                       NULL,
1036                                       gerror)
1037 #else
1038         !e_cal_get_object(m_calendar,
1039                           id.m_uid.c_str(),
1040                           !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
1041                           &comp,
1042                           gerror)
1043 #endif
1044         ) {
1045         if (IsCalObjNotFound(gerror)) {
1046             throwError(STATUS_NOT_FOUND, string("retrieving item: ") + id.getLUID());
1047         } else {
1048             throwError(string("retrieving item: ") + id.getLUID(), gerror);
1049         }
1050     }
1051     if (!comp) {
1052         throwError(string("retrieving item: ") + id.getLUID());
1053     }
1054     eptr<icalcomponent> ptr(comp);
1055
1056     /*
1057      * EDS bug: if a parent doesn't exist while a child does, and we ask
1058      * for the parent, we are sent the (first?) child. Detect this and
1059      * turn it into a "not found" error.
1060      */
1061     if (id.m_rid.empty()) {
1062         struct icaltimetype rid = icalcomponent_get_recurrenceid(comp);
1063         if (!icaltime_is_null_time(rid)) {
1064             throwError(string("retrieving item: got child instead of parent: ") + id.m_uid);
1065         }
1066     }
1067
1068     return ptr.release();
1069 }
1070
1071 string EvolutionCalendarSource::retrieveItemAsString(const ItemID &id)
1072 {
1073     eptr<icalcomponent> comp(retrieveItem(id));
1074     eptr<char> icalstr;
1075
1076 #ifdef USE_ECAL_CLIENT
1077     icalstr = e_cal_client_get_component_as_string(m_calendar, comp);
1078 #else
1079     icalstr = e_cal_get_component_as_string(m_calendar, comp);
1080 #endif
1081
1082     if (!icalstr) {
1083         // One reason why e_cal_get_component_as_string() can fail is
1084         // that it uses a TZID which has no corresponding VTIMEZONE
1085         // definition. Evolution GUI ignores the TZID and interprets
1086         // the times as local time. Do the same when exporting the
1087         // event by removing the bogus TZID.
1088         icalproperty *prop = icalcomponent_get_first_property (comp,
1089                                                                ICAL_ANY_PROPERTY);
1090
1091         while (prop) {
1092             // removes only the *first* TZID - but there shouldn't be more than one
1093             icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
1094             prop = icalcomponent_get_next_property (comp,
1095                                                     ICAL_ANY_PROPERTY);
1096         }
1097
1098         // now try again
1099 #ifdef USE_ECAL_CLIENT
1100         icalstr = e_cal_client_get_component_as_string(m_calendar, comp);
1101 #else
1102         icalstr = e_cal_get_component_as_string(m_calendar, comp);
1103 #endif
1104         if (!icalstr) {
1105             throwError(string("could not encode item as iCalendar: ") + id.getLUID());
1106         } else {
1107             SE_LOG_DEBUG(this, NULL, "had to remove TZIDs because e_cal_get_component_as_string() failed for:\n%s", icalstr.get());
1108         }
1109     }
1110
1111     /*
1112      * Evolution/libical can only deal with \, as separator.
1113      * Replace plain \, in outgoing event CATEGORIES with , -
1114      * based on simple text search/replace and thus will not work
1115      * in all cases...
1116      *
1117      * Inverse operation in insertItem().
1118      */
1119     string data = string(icalstr);
1120     size_t propstart = data.find("\nCATEGORIES");
1121     bool modified = false;
1122     while (propstart != data.npos) {
1123         size_t eol = data.find('\n', propstart + 1);
1124         size_t comma = data.find(',', propstart);
1125
1126         while (eol != data.npos &&
1127                comma != data.npos &&
1128                comma < eol) {
1129             if (data[comma-1] == '\\') {
1130                 data.erase(comma - 1, 1);
1131                 comma--;
1132                 modified = true;
1133             }
1134             comma = data.find(',', comma + 1);
1135         }
1136         propstart = data.find("\nCATEGORIES", propstart + 1);
1137     }
1138     if (modified) {
1139         SE_LOG_DEBUG(this, NULL, "after replacing \\, with , in CATEGORIES:\n%s", data.c_str());
1140     }
1141     
1142     return data;
1143 }
1144
1145 std::string EvolutionCalendarSource::getDescription(const string &luid)
1146 {
1147     try {
1148         eptr<icalcomponent> comp(retrieveItem(ItemID(luid)));
1149         std::string descr;
1150
1151         const char *summary = icalcomponent_get_summary(comp);
1152         if (summary && summary[0]) {
1153             descr += summary;
1154         }
1155         
1156         if (m_type == EVOLUTION_CAL_SOURCE_TYPE_EVENTS) {
1157             const char *location = icalcomponent_get_location(comp);
1158             if (location && location[0]) {
1159                 if (!descr.empty()) {
1160                     descr += ", ";
1161                 }
1162                 descr += location;
1163             }
1164         }
1165
1166         if (m_type == EVOLUTION_CAL_SOURCE_TYPE_MEMOS &&
1167             descr.empty()) {
1168             // fallback to first line of body text
1169             icalproperty *desc = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
1170             if (desc) {
1171                 const char *text = icalproperty_get_description(desc);
1172                 if (text) {
1173                     const char *eol = strchr(text, '\n');
1174                     if (eol) {
1175                         descr.assign(text, eol - text);
1176                     } else {
1177                         descr = text;
1178                     }
1179                 }
1180             }
1181         }
1182
1183         return descr;
1184     } catch (...) {
1185         // Instead of failing we log the error and ask
1186         // the caller to log the UID. That way transient
1187         // errors or errors in the logging code don't
1188         // prevent syncs.
1189         handleException();
1190         return "";
1191     }
1192 }
1193
1194 string EvolutionCalendarSource::ItemID::getLUID() const
1195 {
1196     return getLUID(m_uid, m_rid);
1197 }
1198
1199 string EvolutionCalendarSource::ItemID::getLUID(const string &uid, const string &rid)
1200 {
1201     return uid + "-rid" + rid;
1202 }
1203
1204 EvolutionCalendarSource::ItemID::ItemID(const string &luid)
1205 {
1206     size_t ridoff = luid.rfind("-rid");
1207     if (ridoff != luid.npos) {
1208         const_cast<string &>(m_uid) = luid.substr(0, ridoff);
1209         const_cast<string &>(m_rid) = luid.substr(ridoff + strlen("-rid"));
1210     } else {
1211         const_cast<string &>(m_uid) = luid;
1212     }
1213 }
1214
1215 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(ECalComponent *ecomp)
1216 {
1217     icalcomponent *icomp = e_cal_component_get_icalcomponent(ecomp);
1218     if (!icomp) {
1219         SE_THROW("internal error in getItemID(): ECalComponent without icalcomp");
1220     }
1221     return getItemID(icomp);
1222 }
1223
1224 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(icalcomponent *icomp)
1225 {
1226     const char *uid;
1227     struct icaltimetype rid;
1228
1229     uid = icalcomponent_get_uid(icomp);
1230     rid = icalcomponent_get_recurrenceid(icomp);
1231     return ItemID(uid ? uid : "",
1232                   icalTime2Str(rid));
1233 }
1234
1235 string EvolutionCalendarSource::getItemModTime(ECalComponent *ecomp)
1236 {
1237     struct icaltimetype *modTime;
1238     e_cal_component_get_last_modified(ecomp, &modTime);
1239     eptr<struct icaltimetype, struct icaltimetype, UnrefFree<struct icaltimetype> > modTimePtr(modTime);
1240     if (!modTimePtr) {
1241         return "";
1242     } else {
1243         return icalTime2Str(*modTimePtr.get());
1244     }
1245 }
1246
1247 string EvolutionCalendarSource::getItemModTime(const ItemID &id)
1248 {
1249     eptr<icalcomponent> icomp(retrieveItem(id));
1250     return getItemModTime(icomp);
1251 }
1252
1253 string EvolutionCalendarSource::getItemModTime(icalcomponent *icomp)
1254 {
1255     icalproperty *modprop = icalcomponent_get_first_property(icomp, ICAL_LASTMODIFIED_PROPERTY);
1256     if (!modprop) {
1257         return "";
1258     }
1259     struct icaltimetype modTime = icalproperty_get_lastmodified(modprop);
1260
1261     return icalTime2Str(modTime);
1262 }
1263
1264 string EvolutionCalendarSource::icalTime2Str(const icaltimetype &tt)
1265 {
1266     static const struct icaltimetype null = { 0 };
1267     if (!memcmp(&tt, &null, sizeof(null))) {
1268         return "";
1269     } else {
1270         eptr<char> timestr(ical_strdup(icaltime_as_ical_string(tt)));
1271         if (!timestr) {
1272             SE_THROW("cannot convert to time string");
1273         }
1274         return timestr.get();
1275     }
1276 }
1277
1278 SE_END_CXX
1279
1280 #endif /* ENABLE_ECAL */
1281
1282 #ifdef ENABLE_MODULES
1283 # include "EvolutionCalendarSourceRegister.cpp"
1284 #endif