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 <syncevo/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 bool EvolutionCalendarSource::LUIDs::containsLUID(const ItemID &id) const
51 const_iterator it = findUID(id.m_uid);
53 it->second.find(id.m_rid) != it->second.end();
56 void EvolutionCalendarSource::LUIDs::insertLUID(const ItemID &id)
58 (*this)[id.m_uid].insert(id.m_rid);
61 void EvolutionCalendarSource::LUIDs::eraseLUID(const ItemID &id)
63 iterator it = find(id.m_uid);
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()) {
75 static int granularity()
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.
82 static bool checked = false;
84 // allow setting the delay (used during testing to shorten runtime)
85 const char *delay = getenv("SYNC_EVOLUTION_EVO_CALENDAR_DELAY");
94 EvolutionCalendarSource::EvolutionCalendarSource(EvolutionCalendarSourceType type,
95 const SyncSourceParams ¶ms) :
96 EvolutionSyncSource(params, granularity()),
100 case EVOLUTION_CAL_SOURCE_TYPE_EVENTS:
101 SyncSourceLogging::init(InitList<std::string>("SUMMARY") + "LOCATION",
104 m_typeName = "calendar";
105 #ifndef USE_ECAL_CLIENT
106 m_newSystem = e_cal_new_system_calendar;
109 case EVOLUTION_CAL_SOURCE_TYPE_TASKS:
110 SyncSourceLogging::init(InitList<std::string>("SUMMARY"),
113 m_typeName = "task list";
114 #ifndef USE_ECAL_CLIENT
115 m_newSystem = e_cal_new_system_tasks;
118 case EVOLUTION_CAL_SOURCE_TYPE_MEMOS:
119 SyncSourceLogging::init(InitList<std::string>("SUBJECT"),
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 */;
131 SyncContext::throwError("internal error, invalid calendar type");
136 SyncSource::Databases EvolutionCalendarSource::getDatabases()
138 ESourceList *tmp = NULL;
143 #ifdef USE_ECAL_CLIENT
144 !e_cal_client_get_sources(&tmp, sourceType(), gerror)
146 !e_cal_get_sources(&tmp, sourceType(), gerror)
149 // ignore unspecific errors (like on Maemo with no support for memos)
150 // and continue with empty list (perhaps defaults work)
154 throwError("unable to access backend databases", gerror);
157 ESourceListCXX sources(tmp, false);
160 for (GSList *g = sources ? e_source_list_peek_groups (sources) : NULL;
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() : "",
174 #ifdef USE_ECAL_CLIENT
175 if (result.empty()) {
176 ECalClientCXX calendar = ECalClientCXX::steal(e_cal_client_new_system(sourceType(), NULL));
178 const char *uri = e_client_get_uri (E_CLIENT ((ECalClient*)calendar));
179 result.push_back(Database("<<system>>", uri ? uri : "<<unknown uri>>"));
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>>"));
196 #ifdef USE_ECAL_CLIENT
198 handle_error_cb (EClient */*client*/, const gchar *error_msg, gpointer user_data)
200 EvolutionCalendarSource *that = static_cast<EvolutionCalendarSource *>(user_data);
201 SE_LOG_ERROR(that, NULL, "%s", error_msg);
205 handle_authentication_cb (EClient */*client*/, ECredentials *credentials, gpointer user_data)
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);
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");
216 if (!passwd.empty()) {
217 e_credentials_set (credentials, E_CREDENTIALS_KEY_PASSWORD, passwd.c_str());
226 char *EvolutionCalendarSource::authenticate(const char *prompt,
229 std::string passwd = getPassword();
231 SE_LOG_DEBUG(this, NULL, "authentication requested, prompt \"%s\", key \"%s\" => %s",
233 !passwd.empty() ? "returning configured password" : "no password configured");
234 return !passwd.empty() ? strdup(passwd.c_str()) : NULL;
239 void EvolutionCalendarSource::open()
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
249 #ifdef USE_ECAL_CLIENT
250 if (!e_cal_client_get_sources (&tmp, sourceType(), gerror)) {
251 throwError("unable to access backend databases", gerror);
253 ESourceListCXX sources(tmp, false);
255 string id = getDatabaseID();
256 ESource *source = findSource(sources, id);
257 bool created = false;
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
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++) {
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));
276 throwError(string("not found: '") + id + "'");
280 m_calendar = ECalClientCXX::steal(e_cal_client_new(source, sourceType(), gerror));
284 throwError("create calendar", gerror);
288 g_signal_connect (m_calendar, "backend-error", G_CALLBACK (handle_error_cb), this);
290 // Handle authentication requests from the backend
291 g_signal_connect (m_calendar, "authenticate", G_CALLBACK (handle_authentication_cb), this);
293 if (!e_client_open_sync(E_CLIENT ((ECalClient*)m_calendar), onlyIfExists, NULL, gerror)) {
295 // opening newly created address books often failed, perhaps that also applies to calendars - try again
298 if (!e_client_open_sync(E_CLIENT ((ECalClient*)m_calendar), onlyIfExists, NULL, gerror)) {
299 throwError(string("opening ") + m_typeName , gerror);
302 throwError(string("opening ") + m_typeName , gerror);
307 if (!e_cal_get_sources(&tmp, sourceType(), gerror)) {
308 throwError("unable to access backend databases", gerror);
310 ESourceListCXX sources(tmp, false);
312 string id = getDatabaseID();
313 ESource *source = findSource(sources, id);
314 bool created = false;
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
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++) {
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());
333 throwError(string("not found: '") + id + "'");
336 onlyIfExists = false;
338 m_calendar.set(e_cal_new(source, sourceType()), m_typeName.c_str());
341 e_cal_set_auth_func(m_calendar, eCalAuthFunc, this);
343 if (!e_cal_open(m_calendar, onlyIfExists, gerror)) {
345 // opening newly created address books often failed, perhaps that also applies to calendars - try again
348 if (!e_cal_open(m_calendar, onlyIfExists, gerror)) {
349 throwError(string("opening ") + m_typeName, gerror);
352 throwError(string("opening ") + m_typeName, gerror);
359 g_signal_connect_after(m_calendar,
361 G_CALLBACK(SyncContext::fatalError),
362 (void *)"Evolution Data Server has died unexpectedly, database no longer available.");
365 bool EvolutionCalendarSource::isEmpty()
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();
374 #ifdef USE_ECAL_CLIENT
375 class ECalClientViewSyncHandler {
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)
381 bool processSync(GErrorCXX &gerror)
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);
388 e_cal_client_view_start (m_view, m_error);
390 std::swap(gerror, m_error);
396 e_cal_client_view_stop (m_view, NULL);
399 std::swap(gerror, m_error);
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);
413 static void completed(ECalClientView *ebookview,
415 gpointer user_data) {
416 ECalClientViewSyncHandler *that = (ECalClientViewSyncHandler *)user_data;
417 that->m_error = error;
422 // Process list callback
423 void (*m_processList)(const GSList *list, void *user_data);
425 // Event loop for Async -> Sync
426 EvolutionAsync m_loop;
430 ECalClientView *m_view;
432 // Possible error while watching the view
436 static void list_revisions(const GSList *objects, void *user_data)
438 EvolutionCalendarSource::RevisionMap_t *revisions =
439 static_cast<EvolutionCalendarSource::RevisionMap_t *>(user_data);
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);
448 (*revisions)[luid] = modTime;
453 void EvolutionCalendarSource::listAllItems(RevisionMap_t &revisions)
456 #ifdef USE_ECAL_CLIENT
457 ECalClientView *view;
459 if (!e_cal_client_get_view_sync (m_calendar, "#t", &view, NULL, gerror)) {
460 throwError( "getting the view" , gerror);
462 ECalClientViewCXX viewPtr = ECalClientViewCXX::steal(view);
464 // TODO: Optimization: use set fields_of_interest (UID / REV / LAST-MODIFIED)
466 ECalClientViewSyncHandler handler(viewPtr, list_revisions, &revisions);
467 if (!handler.processSync(gerror)) {
468 throwError("watching view", gerror);
473 RevisionMap_t::iterator it;
474 for(it = revisions.begin(); it != revisions.end(); it++) {
475 m_allLUIDs.insertLUID(it->first);
481 if (!e_cal_get_object_list_as_comp(m_calendar,
485 throwError("reading all items", gerror);
487 eptr<GList> listptr(nextItem);
489 ECalComponent *ecomp = E_CAL_COMPONENT(nextItem->data);
490 ItemID id = getItemID(ecomp);
491 string luid = id.getLUID();
492 string modTime = getItemModTime(ecomp);
494 m_allLUIDs.insertLUID(id);
495 revisions[luid] = modTime;
496 nextItem = nextItem->next;
501 void EvolutionCalendarSource::close()
506 void EvolutionCalendarSource::readItem(const string &luid, std::string &item, bool raw)
509 item = retrieveItemAsString(id);
512 #ifdef USE_ECAL_CLIENT
514 my_tzlookup(const gchar *tzid,
515 gconstpointer ecalclient,
516 GCancellable *cancellable,
519 icaltimezone *zone = NULL;
520 GError *local_error = NULL;
522 if (e_cal_client_get_timezone_sync((ECalClient *)ecalclient, tzid, &zone, cancellable, &local_error)) {
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'
529 g_clear_error (&local_error);
530 } else if (local_error) {
531 g_propagate_error (error, local_error);
538 EvolutionCalendarSource::InsertItemResult EvolutionCalendarSource::insertItem(const string &luid, const std::string &item, bool raw)
540 bool update = !luid.empty();
541 InsertItemResultState state = ITEM_OKAY;
542 bool detached = false;
543 string newluid = luid;
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
553 * Inverse operation in extractItemAsString().
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);
561 while (eol != data.npos &&
562 comma != data.npos &&
564 if (data[comma-1] != '\\') {
565 data.insert(comma, "\\");
569 comma = data.find(',', comma + 1);
571 propstart = data.find("\nCATEGORIES", propstart + 1);
574 SE_LOG_DEBUG(this, NULL, "after replacing , with \\, in CATEGORIES:\n%s", data.c_str());
577 eptr<icalcomponent> icomp(icalcomponent_new_from_string((char *)data.c_str()));
580 throwError(string("failure parsing ical") + data);
587 #ifdef USE_ECAL_CLIENT
588 !e_cal_client_check_timezones(icomp,
591 (const void *)m_calendar.get(),
595 !e_cal_check_timezones(icomp,
598 (const void *)m_calendar.get(),
602 throwError(string("fixing timezones") + data,
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);
610 tcomp = icalcomponent_get_next_component(icomp, ICAL_VTIMEZONE_COMPONENT)) {
611 eptr<icaltimezone> zone(icaltimezone_new(), "icaltimezone");
612 icaltimezone_set_component(zone, tcomp);
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");
621 #ifdef USE_ECAL_CLIENT
622 e_cal_client_add_timezone_sync(m_calendar, zone, NULL, gerror)
624 e_cal_add_timezone(m_calendar, zone, gerror)
628 throwError(string("error adding VTIMEZONE ") + tzid,
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,
640 throwError("extracting event");
643 // Remove LAST-MODIFIED: the Evolution Exchange Connector does not
644 // properly update this property if it is already present in the
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);
654 ItemID id = getItemID(subcomp);
655 const char *uid = NULL;
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.
663 // Existing detached recurrences are silently updated when
664 // trying to add them. This breaks our return code and change
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;
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)) {
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
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);
698 // creating new objects works for normal events and detached occurrences alike
700 #ifdef USE_ECAL_CLIENT
701 e_cal_client_create_object_sync(m_calendar, subcomp, (gchar **)&uid,
704 e_cal_create_object(m_calendar, subcomp, (gchar **)&uid, gerror)
707 #ifdef USE_ECAL_CLIENT
708 PlainGStr owner((gchar *)uid);
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);
718 throwError("storing new item", gerror);
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) {
725 #ifdef USE_ECAL_CLIENT
726 !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
727 CALOBJ_MOD_THIS, NULL,
730 !e_cal_modify_object(m_calendar, *icalcomp,
735 throwError(string("recreating item ") + newluid, gerror);
743 (state != ITEM_OKAY && state != ITEM_NEEDS_MERGE) ||
746 bool isParent = id.m_rid.empty();
748 // ensure that the component has the right UID and
751 if (!id.m_uid.empty()) {
752 icalcomponent_set_uid(subcomp, id.m_uid.c_str());
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);
768 icalcomponent_add_property(subcomp, icalproperty_new_clone(orig_rid));
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
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) {
792 // Use CALOBJ_MOD_ALL and temporarily remove
793 // the children, then add them again. Otherwise they would
795 ICalComps_t children = removeEvents(id.m_uid, true);
797 // Parent is gone, too, and needs to be recreated.
798 const char *uid = NULL;
800 #ifdef USE_ECAL_CLIENT
801 !e_cal_client_create_object_sync(m_calendar, subcomp, (char **)&uid,
804 !e_cal_create_object(m_calendar, subcomp, (char **)&uid, gerror)
807 throwError(string("creating updated item ") + luid, gerror);
809 #ifdef USE_ECAL_CLIENT
810 PlainGStr owner((gchar *)uid);
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) {
817 #ifdef USE_ECAL_CLIENT
818 !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
819 CALOBJ_MOD_THIS, NULL,
822 !e_cal_modify_object(m_calendar, *icalcomp,
827 throwError(string("recreating item ") + luid, gerror);
831 // no children, updating is simple
833 #ifdef USE_ECAL_CLIENT
834 !e_cal_client_modify_object_sync(m_calendar, subcomp,
835 CALOBJ_MOD_ALL, NULL,
838 !e_cal_modify_object(m_calendar, subcomp,
843 throwError(string("updating item ") + luid, gerror);
849 #ifdef USE_ECAL_CLIENT
850 !e_cal_client_modify_object_sync(m_calendar, subcomp,
851 CALOBJ_MOD_THIS, NULL,
854 !e_cal_modify_object(m_calendar, subcomp,
859 throwError(string("updating item ") + luid, gerror);
863 ItemID newid = getItemID(subcomp);
864 newluid = newid.getLUID();
865 modTime = getItemModTime(newid);
868 return InsertItemResult(newluid, modTime, state);
871 EvolutionCalendarSource::ICalComps_t EvolutionCalendarSource::removeEvents(const string &uid, bool returnOnlyChildren, bool ignoreNotFound)
875 LUIDs::const_iterator it = m_allLUIDs.find(uid);
876 if (it != m_allLUIDs.end()) {
877 BOOST_FOREACH(const string &rid, it->second) {
879 icalcomponent *icomp = retrieveItem(id);
881 if (id.m_rid.empty() && returnOnlyChildren) {
882 icalcomponent_free(icomp);
884 events.push_back(ICalComps_t::value_type(new eptr<icalcomponent>(icomp)));
890 // removes all events with that UID, including children
893 #ifdef USE_ECAL_CLIENT
894 !e_cal_client_remove_object_sync(m_calendar,
895 uid.c_str(), NULL, CALOBJ_MOD_ALL,
899 !e_cal_remove_object(m_calendar,
904 if (IsCalObjNotFound(gerror)) {
905 SE_LOG_DEBUG(this, NULL, "%s: request to delete non-existant item ignored",
907 if (!ignoreNotFound) {
908 throwError(STATUS_NOT_FOUND, string("delete item: ") + uid);
911 throwError(string("deleting item " ) + uid, gerror);
918 void EvolutionCalendarSource::removeItem(const string &luid)
923 if (id.m_rid.empty()) {
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.
931 ICalComps_t children = removeEvents(id.m_uid, true, false);
935 BOOST_FOREACH(boost::shared_ptr< eptr<icalcomponent> > &icalcomp, children) {
940 #ifdef USE_ECAL_CLIENT
941 !e_cal_client_create_object_sync(m_calendar, *icalcomp, &uid,
944 !e_cal_create_object(m_calendar, *icalcomp, &uid, gerror)
947 throwError(string("recreating first item ") + luid, gerror);
949 #ifdef USE_ECAL_CLIENT
950 PlainGStr owner((gchar *)uid);
955 #ifdef USE_ECAL_CLIENT
956 !e_cal_client_modify_object_sync(m_calendar, *icalcomp,
957 CALOBJ_MOD_THIS, NULL,
960 !e_cal_modify_object(m_calendar, *icalcomp,
965 throwError(string("recreating following item ") + luid, gerror);
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,
980 CALOBJ_MOD_ONLY_THIS,
984 e_cal_remove_object_with_mod(m_calendar,
992 (!success && IsCalObjNotFound(gerror))) {
993 SE_LOG_DEBUG(this, NULL, "%s: request to delete non-existant item",
995 throwError(STATUS_NOT_FOUND, string("delete item: ") + id.getLUID());
996 } else if (!success) {
997 throwError(string("deleting item " ) + luid, gerror);
1000 m_allLUIDs.eraseLUID(id);
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
1012 ItemID parent(id.m_uid, "");
1013 string modTime = getItemModTime(parent);
1014 string parentLUID = parent.getLUID();
1015 updateRevision(getTrackingNode(), parentLUID, parentLUID, modTime);
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).
1024 icalcomponent *EvolutionCalendarSource::retrieveItem(const ItemID &id)
1027 icalcomponent *comp = NULL;
1030 #ifdef USE_ECAL_CLIENT
1031 !e_cal_client_get_object_sync(m_calendar,
1033 !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
1038 !e_cal_get_object(m_calendar,
1040 !id.m_rid.empty() ? id.m_rid.c_str() : NULL,
1045 if (IsCalObjNotFound(gerror)) {
1046 throwError(STATUS_NOT_FOUND, string("retrieving item: ") + id.getLUID());
1048 throwError(string("retrieving item: ") + id.getLUID(), gerror);
1052 throwError(string("retrieving item: ") + id.getLUID());
1054 eptr<icalcomponent> ptr(comp);
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.
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);
1068 return ptr.release();
1071 string EvolutionCalendarSource::retrieveItemAsString(const ItemID &id)
1073 eptr<icalcomponent> comp(retrieveItem(id));
1076 #ifdef USE_ECAL_CLIENT
1077 icalstr = e_cal_client_get_component_as_string(m_calendar, comp);
1079 icalstr = e_cal_get_component_as_string(m_calendar, comp);
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,
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,
1099 #ifdef USE_ECAL_CLIENT
1100 icalstr = e_cal_client_get_component_as_string(m_calendar, comp);
1102 icalstr = e_cal_get_component_as_string(m_calendar, comp);
1105 throwError(string("could not encode item as iCalendar: ") + id.getLUID());
1107 SE_LOG_DEBUG(this, NULL, "had to remove TZIDs because e_cal_get_component_as_string() failed for:\n%s", icalstr.get());
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
1117 * Inverse operation in insertItem().
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);
1126 while (eol != data.npos &&
1127 comma != data.npos &&
1129 if (data[comma-1] == '\\') {
1130 data.erase(comma - 1, 1);
1134 comma = data.find(',', comma + 1);
1136 propstart = data.find("\nCATEGORIES", propstart + 1);
1139 SE_LOG_DEBUG(this, NULL, "after replacing \\, with , in CATEGORIES:\n%s", data.c_str());
1145 std::string EvolutionCalendarSource::getDescription(const string &luid)
1148 eptr<icalcomponent> comp(retrieveItem(ItemID(luid)));
1151 const char *summary = icalcomponent_get_summary(comp);
1152 if (summary && summary[0]) {
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()) {
1166 if (m_type == EVOLUTION_CAL_SOURCE_TYPE_MEMOS &&
1168 // fallback to first line of body text
1169 icalproperty *desc = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
1171 const char *text = icalproperty_get_description(desc);
1173 const char *eol = strchr(text, '\n');
1175 descr.assign(text, eol - text);
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
1194 string EvolutionCalendarSource::ItemID::getLUID() const
1196 return getLUID(m_uid, m_rid);
1199 string EvolutionCalendarSource::ItemID::getLUID(const string &uid, const string &rid)
1201 return uid + "-rid" + rid;
1204 EvolutionCalendarSource::ItemID::ItemID(const string &luid)
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"));
1211 const_cast<string &>(m_uid) = luid;
1215 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(ECalComponent *ecomp)
1217 icalcomponent *icomp = e_cal_component_get_icalcomponent(ecomp);
1219 SE_THROW("internal error in getItemID(): ECalComponent without icalcomp");
1221 return getItemID(icomp);
1224 EvolutionCalendarSource::ItemID EvolutionCalendarSource::getItemID(icalcomponent *icomp)
1227 struct icaltimetype rid;
1229 uid = icalcomponent_get_uid(icomp);
1230 rid = icalcomponent_get_recurrenceid(icomp);
1231 return ItemID(uid ? uid : "",
1235 string EvolutionCalendarSource::getItemModTime(ECalComponent *ecomp)
1237 struct icaltimetype *modTime;
1238 e_cal_component_get_last_modified(ecomp, &modTime);
1239 eptr<struct icaltimetype, struct icaltimetype, UnrefFree<struct icaltimetype> > modTimePtr(modTime);
1243 return icalTime2Str(*modTimePtr.get());
1247 string EvolutionCalendarSource::getItemModTime(const ItemID &id)
1249 eptr<icalcomponent> icomp(retrieveItem(id));
1250 return getItemModTime(icomp);
1253 string EvolutionCalendarSource::getItemModTime(icalcomponent *icomp)
1255 icalproperty *modprop = icalcomponent_get_first_property(icomp, ICAL_LASTMODIFIED_PROPERTY);
1259 struct icaltimetype modTime = icalproperty_get_lastmodified(modprop);
1261 return icalTime2Str(modTime);
1264 string EvolutionCalendarSource::icalTime2Str(const icaltimetype &tt)
1266 static const struct icaltimetype null = { 0 };
1267 if (!memcmp(&tt, &null, sizeof(null))) {
1270 eptr<char> timestr(ical_strdup(icaltime_as_ical_string(tt)));
1272 SE_THROW("cannot convert to time string");
1274 return timestr.get();
1280 #endif /* ENABLE_ECAL */
1282 #ifdef ENABLE_MODULES
1283 # include "EvolutionCalendarSourceRegister.cpp"