2 * Copyright (C) 2005-2009 Patrick Ohly <patrick.ohly@gmx.de>
3 * Copyright (C) 2009 Intel Corporation
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.
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.
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
28 // include first, it sets HANDLE_LIBICAL_MEMORY for us
29 #include "libical/icalstrdup.h"
31 #include <syncevo/SyncContext.h>
32 #include <syncevo/SmartPtr.h>
33 #include <syncevo/Logging.h>
35 #include "EvolutionCalendarSource.h"
36 #include "EvolutionMemoSource.h"
37 #include "e-cal-check-timezones.h"
40 #include <boost/foreach.hpp>
42 #include <syncevo/declarations.h>
46 EVOLUTION_CALENDAR_PRODID("PRODID:-//ACME//NONSGML SyncEvolution//EN"),
47 EVOLUTION_CALENDAR_VERSION("VERSION:2.0");
49 class unrefECalObjectList {
51 /** free list of ECalChange instances */
52 static void unref(GList *pointer) {
54 e_cal_free_object_list(pointer);
59 static int granularity()
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.
66 static bool checked = false;
68 // allow setting the delay (used during testing to shorten runtime)
69 const char *delay = getenv("SYNC_EVOLUTION_EVO_CALENDAR_DELAY");
78 EvolutionCalendarSource::EvolutionCalendarSource(ECalSourceType type,
79 const SyncSourceParams ¶ms) :
80 EvolutionSyncSource(params, granularity()),
84 case E_CAL_SOURCE_TYPE_EVENT:
85 SyncSourceLogging::init(InitList<std::string>("SUMMARY") + "LOCATION",
88 m_typeName = "calendar";
89 m_newSystem = e_cal_new_system_calendar;
91 case E_CAL_SOURCE_TYPE_TODO:
92 SyncSourceLogging::init(InitList<std::string>("SUMMARY"),
95 m_typeName = "task list";
96 m_newSystem = e_cal_new_system_tasks;
98 case E_CAL_SOURCE_TYPE_JOURNAL:
99 SyncSourceLogging::init(InitList<std::string>("SUBJECT"),
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 */;
109 SyncContext::throwError("internal error, invalid calendar type");
114 SyncSource::Databases EvolutionCalendarSource::getDatabases()
116 ESourceList *sources = NULL;
117 GError *gerror = NULL;
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)
126 throwError("unable to access backend databases", gerror);
131 for (GSList *g = sources ? e_source_list_peek_groups (sources) : NULL;
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() : "",
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>>"));
157 char *EvolutionCalendarSource::authenticate(const char *prompt,
160 const char *passwd = getPassword();
162 SE_LOG_DEBUG(this, NULL, "authentication requested, prompt \"%s\", key \"%s\" => %s",
164 passwd && passwd[0] ? "returning configured password" : "no password configured");
165 return passwd && passwd[0] ? strdup(passwd) : NULL;
168 void EvolutionCalendarSource::open()
170 ESourceList *sources;
171 GError *gerror = NULL;
173 if (!e_cal_get_sources(&sources, m_type, &gerror)) {
174 throwError("unable to access backend databases", gerror);
177 string id = getDatabaseID();
178 ESource *source = findSource(sources, id);
179 bool onlyIfExists = true;
180 bool created = false;
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
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++) {
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());
199 throwError(string("not found: '") + id + "'");
202 onlyIfExists = false;
204 m_calendar.set(e_cal_new(source, m_type), m_typeName.c_str());
207 e_cal_set_auth_func(m_calendar, eCalAuthFunc, this);
209 if (!e_cal_open(m_calendar, onlyIfExists, &gerror)) {
211 // opening newly created address books often failed, perhaps that also applies to calendars - try again
212 g_clear_error(&gerror);
214 if (!e_cal_open(m_calendar, onlyIfExists, &gerror)) {
215 throwError(string("opening ") + m_typeName, gerror );
218 throwError(string("opening ") + m_typeName, gerror );
223 g_signal_connect_after(m_calendar,
225 G_CALLBACK(SyncContext::fatalError),
226 (void *)"Evolution Data Server has died unexpectedly, database no longer available.");
229 bool EvolutionCalendarSource::isEmpty()
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();
238 void EvolutionCalendarSource::listAllItems(RevisionMap_t &revisions)
240 GError *gerror = NULL;
244 if (!e_cal_get_object_list_as_comp(m_calendar,
248 throwError( "reading all items", gerror );
250 eptr<GList> listptr(nextItem);
252 ECalComponent *ecomp = E_CAL_COMPONENT(nextItem->data);
253 ItemID id = getItemID(ecomp);
254 string luid = id.getLUID();
255 string modTime = getItemModTime(ecomp);
257 m_allLUIDs.insert(luid);
258 revisions[luid] = modTime;
259 nextItem = nextItem->next;
263 void EvolutionCalendarSource::close()
268 void EvolutionCalendarSource::readItem(const string &luid, std::string &item, bool raw)
271 item = retrieveItemAsString(id);
274 EvolutionCalendarSource::InsertItemResult EvolutionCalendarSource::insertItem(const string &luid, const std::string &item, bool raw)
276 bool update = !luid.empty();
278 bool detached = false;
279 string newluid = luid;
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
289 * Inverse operation in extractItemAsString().
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);
297 while (eol != data.npos &&
298 comma != data.npos &&
300 if (data[comma-1] != '\\') {
301 data.insert(comma, "\\");
305 comma = data.find(',', comma + 1);
307 propstart = data.find("\nCATEGORIES", propstart + 1);
310 SE_LOG_DEBUG(this, NULL, "after replacing , with \\, in CATEGORIES:\n%s", data.c_str());
313 eptr<icalcomponent> icomp(icalcomponent_new_from_string((char *)data.c_str()));
316 throwError(string("failure parsing ical") + data);
319 GError *gerror = NULL;
322 if (!e_cal_check_timezones(icomp,
325 (const void *)m_calendar.get(),
327 throwError(string("fixing timezones") + data,
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);
335 tcomp = icalcomponent_get_next_component(icomp, ICAL_VTIMEZONE_COMPONENT)) {
336 eptr<icaltimezone> zone(icaltimezone_new(), "icaltimezone");
337 icaltimezone_set_component(zone, tcomp);
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");
345 gboolean success = e_cal_add_timezone(m_calendar, zone, &gerror);
347 throwError(string("error adding VTIMEZONE ") + tzid,
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,
359 throwError("extracting event");
362 // Remove LAST-MODIFIED: the Evolution Exchange Connector does not
363 // properly update this property if it is already present in the
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);
373 ItemID id = getItemID(subcomp);
374 const char *uid = NULL;
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.
382 // Existing detached recurrences are silently updated when
383 // trying to add them. This breaks our return code and change
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()) {
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()) {
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
410 ICalComps_t children;
411 if (id.m_rid.empty()) {
412 children = removeEvents(id.m_uid, true);
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);
425 throwError("storing new item", gerror);
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,
434 throwError(string("recreating item ") + newluid, gerror);
441 if (update || merged || detached) {
443 bool isParent = id.m_rid.empty();
445 // ensure that the component has the right UID and
448 if (!id.m_uid.empty()) {
449 icalcomponent_set_uid(subcomp, id.m_uid.c_str());
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);
465 icalcomponent_add_property(subcomp, icalproperty_new_clone(orig_rid));
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
477 bool hasChildren = false;
478 BOOST_FOREACH(ItemID existingId, m_allLUIDs) {
479 if (existingId.m_uid == id.m_uid &&
480 existingId.m_rid.size()) {
487 // Use CALOBJ_MOD_ALL and temporarily remove
488 // the children, then add them again. Otherwise they would
490 ICalComps_t children = removeEvents(id.m_uid, true);
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);
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,
504 throwError(string("recreating item ") + luid, gerror);
508 // no children, updating is simple
509 if (!e_cal_modify_object(m_calendar, subcomp,
512 throwError(string("updating item ") + luid, gerror);
517 if (!e_cal_modify_object(m_calendar, subcomp,
520 throwError(string("updating item ") + luid, gerror);
524 ItemID newid = getItemID(subcomp);
525 newluid = newid.getLUID();
526 modTime = getItemModTime(newid);
529 return InsertItemResult(newluid, modTime, merged);
532 EvolutionCalendarSource::ICalComps_t EvolutionCalendarSource::removeEvents(const string &uid, bool returnOnlyChildren)
536 BOOST_FOREACH(const string &luid, m_allLUIDs) {
539 if (id.m_uid == uid) {
540 icalcomponent *icomp = retrieveItem(id);
542 if (id.m_rid.empty() && returnOnlyChildren) {
543 icalcomponent_free(icomp);
545 events.push_back(ICalComps_t::value_type(new eptr<icalcomponent>(icomp)));
551 // removes all events with that UID, including children
552 GError *gerror = NULL;
553 if(!e_cal_remove_object(m_calendar,
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",
560 g_clear_error(&gerror);
562 throwError(string("deleting item " ) + uid, gerror);
569 void EvolutionCalendarSource::removeItem(const string &luid)
571 GError *gerror = NULL;
574 if (id.m_rid.empty()) {
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.
582 ICalComps_t children = removeEvents(id.m_uid, true);
585 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
588 if (!e_cal_create_object(m_calendar, *icalcomp, &uid, &gerror)) {
589 throwError(string("recreating item ") + luid, gerror);
592 } else if(!e_cal_remove_object_with_mod(m_calendar,
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);
603 throwError(string("deleting item " ) + luid, gerror);
606 m_allLUIDs.erase(luid);
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
618 ItemID parent(id.m_uid, "");
619 string modTime = getItemModTime(parent);
620 string parentLUID = parent.getLUID();
621 updateRevision(getTrackingNode(), parentLUID, parentLUID, modTime);
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).
630 icalcomponent *EvolutionCalendarSource::retrieveItem(const ItemID &id)
632 GError *gerror = NULL;
633 icalcomponent *comp = NULL;
635 if (!e_cal_get_object(m_calendar,
637 !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
640 throwError(string("retrieving item: ") + id.getLUID(), gerror);
643 throwError(string("retrieving item: ") + id.getLUID());
649 string EvolutionCalendarSource::retrieveItemAsString(const ItemID &id)
651 eptr<icalcomponent> comp(retrieveItem(id));
654 icalstr = e_cal_get_component_as_string(m_calendar, comp);
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,
666 icalparameter *param = icalproperty_get_first_parameter(prop,
667 ICAL_TZID_PARAMETER);
669 icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
670 param = icalproperty_get_next_parameter (prop, ICAL_TZID_PARAMETER);
672 prop = icalcomponent_get_next_property (comp,
677 icalstr = icalcomponent_as_ical_string(comp);
679 throwError(string("could not encode item as iCalendar: ") + id.getLUID());
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
689 * Inverse operation in insertItem().
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);
698 while (eol != data.npos &&
699 comma != data.npos &&
701 if (data[comma-1] == '\\') {
702 data.erase(comma - 1, 1);
706 comma = data.find(',', comma + 1);
708 propstart = data.find("\nCATEGORIES", propstart + 1);
711 SE_LOG_DEBUG(this, NULL, "after replacing \\, with , in CATEGORIES:\n%s", data.c_str());
717 std::string EvolutionCalendarSource::getDescription(const string &luid)
720 eptr<icalcomponent> comp(retrieveItem(ItemID(luid)));
723 const char *summary = icalcomponent_get_summary(comp);
724 if (summary && summary[0]) {
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()) {
738 if (m_type == E_CAL_SOURCE_TYPE_JOURNAL &&
740 // fallback to first line of body text
741 icalproperty *desc = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
743 const char *text = icalproperty_get_description(desc);
745 const char *eol = strchr(text, '\n');
747 descr.assign(text, eol - text);
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
766 string EvolutionCalendarSource::ItemID::getLUID() const
768 return getLUID(m_uid, m_rid);
771 string EvolutionCalendarSource::ItemID::getLUID(const string &uid, const string &rid)
773 return uid + "-rid" + rid;
776 EvolutionCalendarSource::ItemID::ItemID(const string &luid)
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"));
783 const_cast<string &>(m_uid) = luid;
787 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(ECalComponent *ecomp)
789 icalcomponent *icomp = e_cal_component_get_icalcomponent(ecomp);
791 throwError("internal error in getItemID(): ECalComponent without icalcomp");
793 return getItemID(icomp);
796 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(icalcomponent *icomp)
799 struct icaltimetype rid;
801 uid = icalcomponent_get_uid(icomp);
802 rid = icalcomponent_get_recurrenceid(icomp);
803 return ItemID(uid ? uid : "",
807 string EvolutionCalendarSource::getItemModTime(ECalComponent *ecomp)
809 struct icaltimetype *modTime;
810 e_cal_component_get_last_modified(ecomp, &modTime);
811 eptr<struct icaltimetype, struct icaltimetype, UnrefFree<struct icaltimetype> > modTimePtr(modTime);
815 return icalTime2Str(*modTimePtr);
819 string EvolutionCalendarSource::getItemModTime(const ItemID &id)
821 eptr<icalcomponent> icomp(retrieveItem(id));
822 icalproperty *lastModified = icalcomponent_get_first_property(icomp, ICAL_LASTMODIFIED_PROPERTY);
826 struct icaltimetype modTime = icalproperty_get_lastmodified(lastModified);
827 return icalTime2Str(modTime);
831 string EvolutionCalendarSource::icalTime2Str(const icaltimetype &tt)
833 static const struct icaltimetype null = { 0 };
834 if (!memcmp(&tt, &null, sizeof(null))) {
837 eptr<char> timestr(ical_strdup(icaltime_as_ical_string(tt)));
839 throwError("cannot convert to time string");
841 return timestr.get();
847 #endif /* ENABLE_ECAL */
849 #ifdef ENABLE_MODULES
850 # include "EvolutionCalendarSourceRegister.cpp"