Imported Upstream version 1.1.0.99.1
[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 "libical/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 class unrefECalObjectList {
50  public:
51     /** free list of ECalChange instances */
52     static void unref(GList *pointer) {
53         if (pointer) {
54             e_cal_free_object_list(pointer);
55         }
56     }
57 };
58
59 static int granularity()
60 {
61     // This long delay is necessary in combination
62     // with Evolution Exchange Connector: when updating
63     // a child event, it seems to take a while until
64     // the change really is effective.
65     static int secs = 5;
66     static bool checked = false;
67     if (!checked) {
68         // allow setting the delay (used during testing to shorten runtime)
69         const char *delay = getenv("SYNC_EVOLUTION_EVO_CALENDAR_DELAY");
70         if (delay) {
71             secs = atoi(delay);
72         }
73         checked = true;
74     }
75     return secs;
76 }
77
78 EvolutionCalendarSource::EvolutionCalendarSource(ECalSourceType type,
79                                                  const SyncSourceParams &params) :
80     EvolutionSyncSource(params, granularity()),
81     m_type(type)
82 {
83     switch (m_type) {
84      case E_CAL_SOURCE_TYPE_EVENT:
85         SyncSourceLogging::init(InitList<std::string>("SUMMARY") + "LOCATION",
86                                 ", ",
87                                 m_operations);
88         m_typeName = "calendar";
89         m_newSystem = e_cal_new_system_calendar;
90         break;
91      case E_CAL_SOURCE_TYPE_TODO:
92         SyncSourceLogging::init(InitList<std::string>("SUMMARY"),
93                                 ", ",
94                                 m_operations);
95         m_typeName = "task list";
96         m_newSystem = e_cal_new_system_tasks;
97         break;
98      case E_CAL_SOURCE_TYPE_JOURNAL:
99         SyncSourceLogging::init(InitList<std::string>("SUBJECT"),
100                                 ", ",
101                                 m_operations);
102         m_typeName = "memo list";
103         // This is not available in older Evolution versions.
104         // A configure check could detect that, but as this isn't
105         // important the functionality is simply disabled.
106         m_newSystem = NULL /* e_cal_new_system_memos */;
107         break;
108      default:
109         SyncContext::throwError("internal error, invalid calendar type");
110         break;
111     }
112 }
113
114 SyncSource::Databases EvolutionCalendarSource::getDatabases()
115 {
116     ESourceList *sources = NULL;
117     GError *gerror = NULL;
118     Databases result;
119
120     if (!e_cal_get_sources(&sources, m_type, &gerror)) {
121         // ignore unspecific errors (like on Maemo with no support for memos)
122         // and continue with empty list (perhaps defaults work)
123         if (!gerror) {
124             sources = NULL;
125         } else {
126             throwError("unable to access backend databases", gerror);
127         }
128     }
129
130     bool first = true;
131     for (GSList *g = sources ? e_source_list_peek_groups (sources) : NULL;
132          g;
133          g = g->next) {
134         ESourceGroup *group = E_SOURCE_GROUP (g->data);
135         for (GSList *s = e_source_group_peek_sources (group); s; s = s->next) {
136             ESource *source = E_SOURCE (s->data);
137             eptr<char> uri(e_source_get_uri(source));
138             result.push_back(Database(e_source_peek_name(source),
139                                       uri ? uri.get() : "",
140                                       first));
141             first = false;
142         }
143     }
144
145     if (result.empty() && m_newSystem) {
146         eptr<ECal, GObject> calendar(m_newSystem());
147         if (calendar.get()) {
148             // okay, default system database exists
149             const char *uri = e_cal_get_uri(calendar.get());
150             result.push_back(Database("<<system>>", uri ? uri : "<<unknown uri>>"));
151         }
152     }
153
154     return result;
155 }
156
157 char *EvolutionCalendarSource::authenticate(const char *prompt,
158                                             const char *key)
159 {
160     const char *passwd = getPassword();
161
162     SE_LOG_DEBUG(this, NULL, "authentication requested, prompt \"%s\", key \"%s\" => %s",
163                  prompt, key,
164                  passwd && passwd[0] ? "returning configured password" : "no password configured");
165     return passwd && passwd[0] ? strdup(passwd) : NULL;
166 }
167
168 void EvolutionCalendarSource::open()
169 {
170     ESourceList *sources;
171     GError *gerror = NULL;
172
173     if (!e_cal_get_sources(&sources, m_type, &gerror)) {
174         throwError("unable to access backend databases", gerror);
175     }
176
177     string id = getDatabaseID();    
178     ESource *source = findSource(sources, id);
179     bool onlyIfExists = true;
180     bool created = false;
181
182     // Open twice. This solves an issue where Evolution's CalDAV
183     // backend only updates its local cache *after* a sync (= while
184     // closing the calendar?), instead of doing it *before* a sync (in
185     // e_cal_open()).
186     //
187     // This workaround is applied to *all* backends because there might
188     // be others with similar problems and for local storage it is
189     // a reasonably cheap operation (so no harm there).
190     for (int retries = 0; retries < 2; retries++) {
191         if (!source) {
192             // might have been special "<<system>>" or "<<default>>", try that and
193             // creating address book from file:// URI before giving up
194             if ((id.empty() || id == "<<system>>") && m_newSystem) {
195                 m_calendar.set(m_newSystem(), (string("system ") + m_typeName).c_str());
196             } else if (!id.compare(0, 7, "file://")) {
197                 m_calendar.set(e_cal_new_from_uri(id.c_str(), m_type), (string("creating ") + m_typeName).c_str());
198             } else {
199                 throwError(string("not found: '") + id + "'");
200             }
201             created = true;
202             onlyIfExists = false;
203         } else {
204             m_calendar.set(e_cal_new(source, m_type), m_typeName.c_str());
205         }
206
207         e_cal_set_auth_func(m_calendar, eCalAuthFunc, this);
208     
209         if (!e_cal_open(m_calendar, onlyIfExists, &gerror)) {
210             if (created) {
211                 // opening newly created address books often failed, perhaps that also applies to calendars - try again
212                 g_clear_error(&gerror);
213                 sleep(5);
214                 if (!e_cal_open(m_calendar, onlyIfExists, &gerror)) {
215                     throwError(string("opening ") + m_typeName, gerror );
216                 }
217             } else {
218                 throwError(string("opening ") + m_typeName, gerror );
219             }
220         }
221     }
222
223     g_signal_connect_after(m_calendar,
224                            "backend-died",
225                            G_CALLBACK(SyncContext::fatalError),
226                            (void *)"Evolution Data Server has died unexpectedly, database no longer available.");
227 }
228
229 bool EvolutionCalendarSource::isEmpty()
230 {
231     // TODO: add more efficient implementation which does not
232     // depend on actually pulling all items from EDS
233     RevisionMap_t revisions;
234     listAllItems(revisions);
235     return revisions.empty();
236 }
237
238 void EvolutionCalendarSource::listAllItems(RevisionMap_t &revisions)
239 {
240     GError *gerror = NULL;
241     GList *nextItem;
242
243     m_allLUIDs.clear();
244     if (!e_cal_get_object_list_as_comp(m_calendar,
245                                        "#t",
246                                        &nextItem,
247                                        &gerror)) {
248         throwError( "reading all items", gerror );
249     }
250     eptr<GList> listptr(nextItem);
251     while (nextItem) {
252         ECalComponent *ecomp = E_CAL_COMPONENT(nextItem->data);
253         ItemID id = getItemID(ecomp);
254         string luid = id.getLUID();
255         string modTime = getItemModTime(ecomp);
256
257         m_allLUIDs.insert(luid);
258         revisions[luid] = modTime;
259         nextItem = nextItem->next;
260     }
261 }
262
263 void EvolutionCalendarSource::close()
264 {
265     m_calendar = NULL;
266 }
267
268 void EvolutionCalendarSource::readItem(const string &luid, std::string &item, bool raw)
269 {
270     ItemID id(luid);
271     item = retrieveItemAsString(id);
272 }
273
274 EvolutionCalendarSource::InsertItemResult EvolutionCalendarSource::insertItem(const string &luid, const std::string &item, bool raw)
275 {
276     bool update = !luid.empty();
277     bool merged = false;
278     bool detached = false;
279     string newluid = luid;
280     string data = item;
281     string modTime;
282
283     /*
284      * Evolution/libical can only deal with \, as separator.
285      * Replace plain , in incoming event CATEGORIES with \, -
286      * based on simple text search/replace and thus will not work
287      * in all cases...
288      *
289      * Inverse operation in extractItemAsString().
290      */
291     size_t propstart = data.find("\nCATEGORIES");
292     bool modified = false;
293     while (propstart != data.npos) {
294         size_t eol = data.find('\n', propstart + 1);
295         size_t comma = data.find(',', propstart);
296
297         while (eol != data.npos &&
298                comma != data.npos &&
299                comma < eol) {
300             if (data[comma-1] != '\\') {
301                 data.insert(comma, "\\");
302                 comma++;
303                 modified = true;
304             }
305             comma = data.find(',', comma + 1);
306         }
307         propstart = data.find("\nCATEGORIES", propstart + 1);
308     }
309     if (modified) {
310         SE_LOG_DEBUG(this, NULL, "after replacing , with \\, in CATEGORIES:\n%s", data.c_str());
311     }
312
313     eptr<icalcomponent> icomp(icalcomponent_new_from_string((char *)data.c_str()));
314
315     if( !icomp ) {
316         throwError(string("failure parsing ical") + data);
317     }
318
319     GError *gerror = NULL;
320
321     // fix up TZIDs
322     if (!e_cal_check_timezones(icomp,
323                                NULL,
324                                e_cal_tzlookup_ecal,
325                                (const void *)m_calendar.get(),
326                                &gerror)) {
327         throwError(string("fixing timezones") + data,
328                    gerror);
329     }
330
331     // insert before adding/updating the event so that the new VTIMEZONE is
332     // immediately available should anyone want it
333     for (icalcomponent *tcomp = icalcomponent_get_first_component(icomp, ICAL_VTIMEZONE_COMPONENT);
334          tcomp;
335          tcomp = icalcomponent_get_next_component(icomp, ICAL_VTIMEZONE_COMPONENT)) {
336         eptr<icaltimezone> zone(icaltimezone_new(), "icaltimezone");
337         icaltimezone_set_component(zone, tcomp);
338
339         GError *gerror = NULL;
340         const char *tzid = icaltimezone_get_tzid(zone);
341         if (!tzid || !tzid[0]) {
342             // cannot add a VTIMEZONE without TZID
343             SE_LOG_DEBUG(this, NULL, "skipping VTIMEZONE without TZID");
344         } else {
345             gboolean success = e_cal_add_timezone(m_calendar, zone, &gerror);
346             if (!success) {
347                 throwError(string("error adding VTIMEZONE ") + tzid,
348                            gerror);
349             }
350         }
351     }
352
353     // the component to update/add must be the
354     // ICAL_VEVENT/VTODO_COMPONENT of the item,
355     // e_cal_create/modify_object() fail otherwise
356     icalcomponent *subcomp = icalcomponent_get_first_component(icomp,
357                                                                getCompType());
358     if (!subcomp) {
359         throwError("extracting event");
360     }
361
362     // Remove LAST-MODIFIED: the Evolution Exchange Connector does not
363     // properly update this property if it is already present in the
364     // incoming data.
365     icalproperty *modprop;
366     while ((modprop = icalcomponent_get_first_property(subcomp, ICAL_LASTMODIFIED_PROPERTY)) != NULL) {
367         icalcomponent_remove_property(subcomp, modprop);
368         icalproperty_free(modprop);
369         modprop = NULL;
370     }
371
372     if (!update) {
373         ItemID id = getItemID(subcomp);
374         const char *uid = NULL;
375
376         // Trying to add a normal event which already exists leads to a
377         // gerror->domain == E_CALENDAR_ERROR
378         // gerror->code == E_CALENDAR_STATUS_OBJECT_ID_ALREADY_EXISTS
379         // error. Depending on the Evolution version, the subcomp
380         // UID gets removed (>= 2.12) or remains unchanged.
381         //
382         // Existing detached recurrences are silently updated when
383         // trying to add them. This breaks our return code and change
384         // tracking.
385         //
386         // Escape this madness by checking the existence ourselve first
387         // based on our list of existing LUIDs. Note that this list is
388         // not updated during a sync. This is correct as long as no LUID
389         // gets used twice during a sync (examples: add + add, delete + add),
390         // which should never happen.
391         newluid = id.getLUID();
392         if (m_allLUIDs.find(newluid) != m_allLUIDs.end()) {
393             merged = true;
394         } else {
395             // if this is a detached recurrence, then we
396             // must use e_cal_modify_object() below if
397             // the parent already exists
398             if (!id.m_rid.empty() &&
399                 m_allLUIDs.find(ItemID::getLUID(id.m_uid, "")) != m_allLUIDs.end()) {
400                 detached = true;
401             } else {
402                 // Creating the parent while children are already in
403                 // the calendar confuses EDS (at least 2.12): the
404                 // parent is stored in the .ics with the old UID, but
405                 // the uid returned to the caller is a different
406                 // one. Retrieving the item then fails. Avoid this
407                 // problem by removing the children from the calendar,
408                 // adding the parent, then updating it with the
409                 // saved children.
410                 ICalComps_t children;
411                 if (id.m_rid.empty()) {
412                     children = removeEvents(id.m_uid, true);
413                 }
414
415                 // creating new objects works for normal events and detached occurrences alike
416                 if(e_cal_create_object(m_calendar, subcomp, (char **)&uid, &gerror)) {
417                     // Evolution workaround: don't rely on uid being set if we already had
418                     // one. In Evolution 2.12.1 it was set to garbage. The recurrence ID
419                     // shouldn't have changed either.
420                     ItemID newid(!id.m_uid.empty() ? id.m_uid : uid, id.m_rid);
421                     newluid = newid.getLUID();
422                     modTime = getItemModTime(newid);
423                     m_allLUIDs.insert(newluid);
424                 } else {
425                     throwError("storing new item", gerror);
426                 }
427
428                 // Recreate any children removed earlier: when we get here,
429                 // the parent exists and we must update it.
430                 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
431                     if (!e_cal_modify_object(m_calendar, *icalcomp,
432                                              CALOBJ_MOD_THIS,
433                                              &gerror)) {
434                         throwError(string("recreating item ") + newluid, gerror);
435                     }
436                 }
437             }
438         }
439     }
440
441     if (update || merged || detached) {
442         ItemID id(newluid);
443         bool isParent = id.m_rid.empty();
444
445         // ensure that the component has the right UID and
446         // RECURRENCE-ID
447         if (update) {
448             if (!id.m_uid.empty()) {
449                 icalcomponent_set_uid(subcomp, id.m_uid.c_str());
450             }
451             if (!id.m_rid.empty()) {
452                 // Reconstructing the RECURRENCE-ID is non-trivial,
453                 // because our luid only contains the date-time, but
454                 // not the time zone. Only do the work if the event
455                 // really doesn't have a RECURRENCE-ID.
456                 struct icaltimetype rid;
457                 rid = icalcomponent_get_recurrenceid(subcomp);
458                 if (icaltime_is_null_time(rid)) {
459                     // Preserve the original RECURRENCE-ID, including
460                     // timezone, no matter what the update contains
461                     // (might have wrong timezone or UTC).
462                     eptr<icalcomponent> orig(retrieveItem(id));
463                     icalproperty *orig_rid = icalcomponent_get_first_property(orig, ICAL_RECURRENCEID_PROPERTY);
464                     if (orig_rid) {
465                         icalcomponent_add_property(subcomp, icalproperty_new_clone(orig_rid));
466                     }
467                 }
468             }
469         }
470
471         if (isParent) {
472             // CALOBJ_MOD_THIS for parent items (UID set, no RECURRENCE-ID)
473             // is not supported by all backends: the Exchange Connector
474             // fails with it. It might be an incorrect usage of the API.
475             // Therefore we have to use CALOBJ_MOD_ALL, but that removes
476             // children.
477             bool hasChildren = false;
478             BOOST_FOREACH(ItemID existingId, m_allLUIDs) {
479                 if (existingId.m_uid == id.m_uid &&
480                     existingId.m_rid.size()) {
481                     hasChildren = true;
482                     break;
483                 }
484             }
485
486             if (hasChildren) {
487                 // Use CALOBJ_MOD_ALL and temporarily remove
488                 // the children, then add them again. Otherwise they would
489                 // get deleted.
490                 ICalComps_t children = removeEvents(id.m_uid, true);
491
492                 // Parent is gone, too, and needs to be recreated.
493                 const char *uid = NULL;
494                 if(!e_cal_create_object(m_calendar, subcomp, (char **)&uid, &gerror)) {
495                     throwError(string("creating updated item ") + luid, gerror);
496                 }
497
498                 // Recreate any children removed earlier: when we get here,
499                 // the parent exists and we must update it.
500                 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
501                     if (!e_cal_modify_object(m_calendar, *icalcomp,
502                                              CALOBJ_MOD_THIS,
503                                              &gerror)) {
504                         throwError(string("recreating item ") + luid, gerror);
505                     }
506                 }
507             } else {
508                 // no children, updating is simple
509                 if (!e_cal_modify_object(m_calendar, subcomp,
510                                          CALOBJ_MOD_ALL,
511                                          &gerror)) {
512                     throwError(string("updating item ") + luid, gerror);
513                 }
514             }
515         } else {
516             // child event
517             if (!e_cal_modify_object(m_calendar, subcomp,
518                                      CALOBJ_MOD_THIS,
519                                      &gerror)) {
520                 throwError(string("updating item ") + luid, gerror);
521             }
522         }
523
524         ItemID newid = getItemID(subcomp);
525         newluid = newid.getLUID();
526         modTime = getItemModTime(newid);
527     }
528
529     return InsertItemResult(newluid, modTime, merged);
530 }
531
532 EvolutionCalendarSource::ICalComps_t EvolutionCalendarSource::removeEvents(const string &uid, bool returnOnlyChildren)
533 {
534     ICalComps_t events;
535
536     BOOST_FOREACH(const string &luid, m_allLUIDs) {
537         ItemID id(luid);
538
539         if (id.m_uid == uid) {
540             icalcomponent *icomp = retrieveItem(id);
541             if (icomp) {
542                 if (id.m_rid.empty() && returnOnlyChildren) {
543                     icalcomponent_free(icomp);
544                 } else {
545                     events.push_back(ICalComps_t::value_type(new eptr<icalcomponent>(icomp)));
546                 }
547             }
548         }
549     }
550
551     // removes all events with that UID, including children
552     GError *gerror = NULL;
553     if(!e_cal_remove_object(m_calendar,
554                             uid.c_str(),
555                             &gerror)) {
556         if (gerror->domain == E_CALENDAR_ERROR &&
557             gerror->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND) {
558             SE_LOG_DEBUG(this, NULL, "%s: request to delete non-existant item ignored",
559                          uid.c_str());
560             g_clear_error(&gerror);
561         } else {
562             throwError(string("deleting item " ) + uid, gerror);
563         }
564     }
565
566     return events;
567 }
568
569 void EvolutionCalendarSource::removeItem(const string &luid)
570 {
571     GError *gerror = NULL;
572     ItemID id(luid);
573
574     if (id.m_rid.empty()) {
575         /*
576          * Removing the parent item also removes all children. Evolution
577          * does that automatically. Calling e_cal_remove_object_with_mod()
578          * without valid rid confuses Evolution, don't do it. As a workaround
579          * remove all items with the given uid and if we only wanted to
580          * delete the parent, then recreate the children.
581          */
582         ICalComps_t children = removeEvents(id.m_uid, true);
583
584         // recreate children
585         BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
586             char *uid;
587
588             if (!e_cal_create_object(m_calendar, *icalcomp, &uid, &gerror)) {
589                 throwError(string("recreating item ") + luid, gerror);
590             }
591         }
592     } else if(!e_cal_remove_object_with_mod(m_calendar,
593                                             id.m_uid.c_str(),
594                                             id.m_rid.c_str(),
595                                             CALOBJ_MOD_THIS,
596                                             &gerror)) {
597         if (gerror->domain == E_CALENDAR_ERROR &&
598             gerror->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND) {
599             SE_LOG_DEBUG(this, NULL, "%s: %s: request to delete non-existant item ignored",
600                       getName(), luid.c_str());
601             g_clear_error(&gerror);
602         } else {
603             throwError(string("deleting item " ) + luid, gerror);
604         }
605     }
606     m_allLUIDs.erase(luid);
607
608     if (!id.m_rid.empty()) {
609         // Removing the child may have modified the parent.
610         // We must record the new LAST-MODIFIED string,
611         // otherwise it might be reported as modified during
612         // the next sync (timing dependent: if the parent
613         // was updated before removing the child *and* the
614         // update and remove fall into the same second,
615         // then the modTime does not change again during the
616         // removal).
617         try {
618             ItemID parent(id.m_uid, "");
619             string modTime = getItemModTime(parent);
620             string parentLUID = parent.getLUID();
621             updateRevision(getTrackingNode(), parentLUID, parentLUID, modTime);
622         } catch (...) {
623             // There's no guarantee that the parent still exists.
624             // Instead of checking that, ignore errors (a bit hacky,
625             // but better than breaking the removal).
626         }
627     }
628 }
629
630 icalcomponent *EvolutionCalendarSource::retrieveItem(const ItemID &id)
631 {
632     GError *gerror = NULL;
633     icalcomponent *comp = NULL;
634
635     if (!e_cal_get_object(m_calendar,
636                           id.m_uid.c_str(),
637                           !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
638                           &comp,
639                           &gerror)) {
640         throwError(string("retrieving item: ") + id.getLUID(), gerror);
641     }
642     if (!comp) {
643         throwError(string("retrieving item: ") + id.getLUID());
644     }
645
646     return comp;
647 }
648
649 string EvolutionCalendarSource::retrieveItemAsString(const ItemID &id)
650 {
651     eptr<icalcomponent> comp(retrieveItem(id));
652     eptr<char> icalstr;
653
654     icalstr = e_cal_get_component_as_string(m_calendar, comp);
655
656     if (!icalstr) {
657         // One reason why e_cal_get_component_as_string() can fail is
658         // that it uses a TZID which has no corresponding VTIMEZONE
659         // definition. Evolution GUI ignores the TZID and interprets
660         // the times as local time. Do the same when exporting the
661         // event by removing the bogus TZID.
662         icalproperty *prop = icalcomponent_get_first_property (comp,
663                                                                ICAL_ANY_PROPERTY);
664
665         while (prop) {
666             icalparameter *param = icalproperty_get_first_parameter(prop,
667                                                                     ICAL_TZID_PARAMETER);
668             while (param) {
669                 icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
670                 param = icalproperty_get_next_parameter (prop, ICAL_TZID_PARAMETER);
671             }
672             prop = icalcomponent_get_next_property (comp,
673                                                     ICAL_ANY_PROPERTY);
674         }
675
676         // now try again
677         icalstr = icalcomponent_as_ical_string(comp);
678         if (!icalstr) {
679             throwError(string("could not encode item as iCalendar: ") + id.getLUID());
680         }
681     }
682
683     /*
684      * Evolution/libical can only deal with \, as separator.
685      * Replace plain \, in outgoing event CATEGORIES with , -
686      * based on simple text search/replace and thus will not work
687      * in all cases...
688      *
689      * Inverse operation in insertItem().
690      */
691     string data = string(icalstr);
692     size_t propstart = data.find("\nCATEGORIES");
693     bool modified = false;
694     while (propstart != data.npos) {
695         size_t eol = data.find('\n', propstart + 1);
696         size_t comma = data.find(',', propstart);
697
698         while (eol != data.npos &&
699                comma != data.npos &&
700                comma < eol) {
701             if (data[comma-1] == '\\') {
702                 data.erase(comma - 1, 1);
703                 comma--;
704                 modified = true;
705             }
706             comma = data.find(',', comma + 1);
707         }
708         propstart = data.find("\nCATEGORIES", propstart + 1);
709     }
710     if (modified) {
711         SE_LOG_DEBUG(this, NULL, "after replacing \\, with , in CATEGORIES:\n%s", data.c_str());
712     }
713     
714     return data;
715 }
716
717 std::string EvolutionCalendarSource::getDescription(const string &luid)
718 {
719     try {
720         eptr<icalcomponent> comp(retrieveItem(ItemID(luid)));
721         std::string descr;
722
723         const char *summary = icalcomponent_get_summary(comp);
724         if (summary && summary[0]) {
725             descr += summary;
726         }
727         
728         if (m_type == E_CAL_SOURCE_TYPE_EVENT) {
729             const char *location = icalcomponent_get_location(comp);
730             if (location && location[0]) {
731                 if (!descr.empty()) {
732                     descr += ", ";
733                 }
734                 descr += location;
735             }
736         }
737
738         if (m_type == E_CAL_SOURCE_TYPE_JOURNAL &&
739             descr.empty()) {
740             // fallback to first line of body text
741             icalproperty *desc = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
742             if (desc) {
743                 const char *text = icalproperty_get_description(desc);
744                 if (text) {
745                     const char *eol = strchr(text, '\n');
746                     if (eol) {
747                         descr.assign(text, eol - text);
748                     } else {
749                         descr = text;
750                     }
751                 }
752             }
753         }
754
755         return descr;
756     } catch (...) {
757         // Instead of failing we log the error and ask
758         // the caller to log the UID. That way transient
759         // errors or errors in the logging code don't
760         // prevent syncs.
761         handleException();
762         return "";
763     }
764 }
765
766 string EvolutionCalendarSource::ItemID::getLUID() const
767 {
768     return getLUID(m_uid, m_rid);
769 }
770
771 string EvolutionCalendarSource::ItemID::getLUID(const string &uid, const string &rid)
772 {
773     return uid + "-rid" + rid;
774 }
775
776 EvolutionCalendarSource::ItemID::ItemID(const string &luid)
777 {
778     size_t ridoff = luid.rfind("-rid");
779     if (ridoff != luid.npos) {
780         const_cast<string &>(m_uid) = luid.substr(0, ridoff);
781         const_cast<string &>(m_rid) = luid.substr(ridoff + strlen("-rid"));
782     } else {
783         const_cast<string &>(m_uid) = luid;
784     }
785 }
786
787 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(ECalComponent *ecomp)
788 {
789     icalcomponent *icomp = e_cal_component_get_icalcomponent(ecomp);
790     if (!icomp) {
791         throwError("internal error in getItemID(): ECalComponent without icalcomp");
792     }
793     return getItemID(icomp);
794 }
795
796 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(icalcomponent *icomp)
797 {
798     const char *uid;
799     struct icaltimetype rid;
800
801     uid = icalcomponent_get_uid(icomp);
802     rid = icalcomponent_get_recurrenceid(icomp);
803     return ItemID(uid ? uid : "",
804                   icalTime2Str(rid));
805 }
806
807 string EvolutionCalendarSource::getItemModTime(ECalComponent *ecomp)
808 {
809     struct icaltimetype *modTime;
810     e_cal_component_get_last_modified(ecomp, &modTime);
811     eptr<struct icaltimetype, struct icaltimetype, UnrefFree<struct icaltimetype> > modTimePtr(modTime);
812     if (!modTimePtr) {
813         return "";
814     } else {
815         return icalTime2Str(*modTimePtr);
816     }
817 }
818
819 string EvolutionCalendarSource::getItemModTime(const ItemID &id)
820 {
821     eptr<icalcomponent> icomp(retrieveItem(id));
822     icalproperty *lastModified = icalcomponent_get_first_property(icomp, ICAL_LASTMODIFIED_PROPERTY);
823     if (!lastModified) {
824         return "";
825     } else {
826         struct icaltimetype modTime = icalproperty_get_lastmodified(lastModified);
827         return icalTime2Str(modTime);
828     }
829 }
830
831 string EvolutionCalendarSource::icalTime2Str(const icaltimetype &tt)
832 {
833     static const struct icaltimetype null = { 0 };
834     if (!memcmp(&tt, &null, sizeof(null))) {
835         return "";
836     } else {
837         eptr<char> timestr(ical_strdup(icaltime_as_ical_string(tt)));
838         if (!timestr) {
839             throwError("cannot convert to time string");
840         }
841         return timestr.get();
842     }
843 }
844
845 SE_END_CXX
846
847 #endif /* ENABLE_ECAL */
848
849 #ifdef ENABLE_MODULES
850 # include "EvolutionCalendarSourceRegister.cpp"
851 #endif