Fix FSF address (Tobias Mueller, #470445)
[platform/upstream/evolution-data-server.git] / servers / exchange / lib / e2k-context.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2
3 /* Copyright (C) 2001-2004 Novell, Inc.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of version 2 of the GNU Lesser General Public
7  * License as published by the Free Software Foundation.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this program; if not, write to the
16  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23
24 #include <ctype.h>
25 #include <errno.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <time.h>
29 #include <unistd.h>
30 #include <sys/types.h>
31 #include <glib.h>
32
33 #ifndef G_OS_WIN32
34 #include <sys/socket.h>
35 #include <netinet/in.h>
36 #include <arpa/inet.h>
37 #else
38 #include <winsock2.h>
39 #include <ws2tcpip.h>
40 #include <windns.h>
41 #endif
42
43 #include "e2k-context.h"
44 #include "e2k-encoding-utils.h"
45 #include "e2k-marshal.h"
46 #include "e2k-propnames.h"
47 #include "e2k-restriction.h"
48 #include "e2k-uri.h"
49 #include "e2k-utils.h"
50 #include "e2k-xml-utils.h"
51
52 #include <libsoup/soup-address.h>
53 #include <libsoup/soup-message-filter.h>
54 #include <libsoup/soup-session-async.h>
55 #include <libsoup/soup-session-sync.h>
56 #include <libsoup/soup-socket.h>
57 #include <libsoup/soup-uri.h>
58 #include <libxml/parser.h>
59 #include <libxml/tree.h>
60 #include <libxml/xmlmemory.h>
61
62 #ifdef G_OS_WIN32
63 /* The strtok() in Microsoft's C library is MT-safe (not stateless,
64  * but that is not needed here).
65  */
66 #define strtok_r(s,sep,lasts ) (*(lasts) = strtok((s),(sep)))
67 #endif
68
69 #ifdef G_OS_WIN32
70 #define CLOSE_SOCKET(socket) closesocket (socket)
71 #define STATUS_IS_SOCKET_ERROR(status) ((status) == SOCKET_ERROR)
72 #define SOCKET_IS_INVALID(socket) ((socket) == INVALID_SOCKET)
73 #define BIND_STATUS_IS_ADDRINUSE() (WSAGetLastError () == WSAEADDRINUSE)
74 #else
75 #define CLOSE_SOCKET(socket) close (socket)
76 #define STATUS_IS_SOCKET_ERROR(status) ((status) == -1)
77 #define SOCKET_IS_INVALID(socket) ((socket) < 0)
78 #define BIND_STATUS_IS_ADDRINUSE() (errno == EADDRINUSE)
79 #endif
80
81 #define PARENT_TYPE G_TYPE_OBJECT
82 static GObjectClass *parent_class;
83
84 enum {
85         REDIRECT,
86         LAST_SIGNAL
87 };
88
89 static guint signals [LAST_SIGNAL] = { 0 };
90
91 struct _E2kContextPrivate {
92         SoupSession *session, *async_session;
93         char *owa_uri, *username, *password;
94         time_t last_timestamp;
95
96         /* Notification listener */
97         SoupSocket *get_local_address_sock;
98         GIOChannel *listener_channel;
99         int listener_watch_id;
100
101         char *notification_uri;
102         GHashTable *subscriptions_by_id, *subscriptions_by_uri;
103
104         /* Forms-based authentication */
105         char *cookie;
106         gboolean cookie_verified;
107 };
108
109 /* For operations with progress */
110 #define E2K_CONTEXT_MIN_BATCH_SIZE 25
111 #define E2K_CONTEXT_MAX_BATCH_SIZE 100
112
113 /* For soup sync session timeout */
114 #define E2K_SOUP_SESSION_TIMEOUT 30
115
116 #ifdef E2K_DEBUG
117 char *e2k_debug;
118 int e2k_debug_level;
119 #endif
120
121 static gboolean renew_subscription (gpointer user_data);
122 static void unsubscribe_internal (E2kContext *ctx, const char *uri, GList *sub_list);
123 static gboolean do_notification (GIOChannel *source, GIOCondition condition, gpointer data);
124
125 static void setup_message (SoupMessageFilter *filter, SoupMessage *msg);
126
127 static void
128 init (GObject *object)
129 {
130         E2kContext *ctx = E2K_CONTEXT (object);
131
132         ctx->priv = g_new0 (E2kContextPrivate, 1);
133         ctx->priv->subscriptions_by_id =
134                 g_hash_table_new (g_str_hash, g_str_equal);
135         ctx->priv->subscriptions_by_uri =
136                 g_hash_table_new (g_str_hash, g_str_equal);
137 }
138
139 static void
140 destroy_sub_list (gpointer uri, gpointer sub_list, gpointer ctx)
141 {
142         unsubscribe_internal (ctx, uri, sub_list);
143         g_list_free (sub_list);
144 }
145
146 static void
147 dispose (GObject *object)
148 {
149         E2kContext *ctx = E2K_CONTEXT (object);
150
151         if (ctx->priv) {
152                 if (ctx->priv->owa_uri)
153                         g_free (ctx->priv->owa_uri);
154                 if (ctx->priv->username)
155                         g_free (ctx->priv->username);
156                 if (ctx->priv->password)
157                         g_free (ctx->priv->password);
158
159                 if (ctx->priv->get_local_address_sock)
160                         g_object_unref (ctx->priv->get_local_address_sock);
161
162                 g_hash_table_foreach (ctx->priv->subscriptions_by_uri,
163                                       destroy_sub_list, ctx);
164                 g_hash_table_destroy (ctx->priv->subscriptions_by_uri);
165
166                 g_hash_table_destroy (ctx->priv->subscriptions_by_id);
167
168                 if (ctx->priv->listener_watch_id)
169                         g_source_remove (ctx->priv->listener_watch_id);
170                 if (ctx->priv->listener_channel) {
171                         g_io_channel_shutdown (ctx->priv->listener_channel,
172                                                FALSE, NULL);
173                         g_io_channel_unref (ctx->priv->listener_channel);
174                 }
175
176                 if (ctx->priv->session)
177                         g_object_unref (ctx->priv->session);
178                 if (ctx->priv->async_session)
179                         g_object_unref (ctx->priv->async_session);
180
181                 g_free (ctx->priv->cookie);
182
183                 g_free (ctx->priv);
184                 ctx->priv = NULL;
185         }
186
187         G_OBJECT_CLASS (parent_class)->dispose (object);
188 }
189
190 static void
191 class_init (GObjectClass *object_class)
192 {
193         parent_class = g_type_class_ref (PARENT_TYPE);
194
195         /* virtual method override */
196         object_class->dispose = dispose;
197
198         signals[REDIRECT] =
199                 g_signal_new ("redirect",
200                               G_OBJECT_CLASS_TYPE (object_class),
201                               G_SIGNAL_RUN_LAST,
202                               G_STRUCT_OFFSET (E2kContextClass, redirect),
203                               NULL, NULL,
204                               e2k_marshal_NONE__INT_STRING_STRING,
205                               G_TYPE_NONE, 3,
206                               G_TYPE_INT,
207                               G_TYPE_STRING,
208                               G_TYPE_STRING);
209 }
210
211 static void
212 filter_iface_init (SoupMessageFilterClass *filter_class)
213 {
214         /* interface implementation */
215         filter_class->setup_message = setup_message;
216
217 #ifdef E2K_DEBUG
218         e2k_debug = getenv ("E2K_DEBUG");
219         if (e2k_debug)
220                 e2k_debug_level = atoi (e2k_debug);
221 #endif
222 }
223
224 E2K_MAKE_TYPE_WITH_IFACE (e2k_context, E2kContext, class_init, init, PARENT_TYPE, filter_iface_init, SOUP_TYPE_MESSAGE_FILTER)
225
226
227 static void
228 renew_sub_list (gpointer key, gpointer value, gpointer data)
229 {
230         GList *sub_list;
231
232         for (sub_list = value; sub_list; sub_list = sub_list->next)
233                 renew_subscription (sub_list->data);
234 }
235
236 static void
237 got_connection (SoupSocket *sock, guint status, gpointer user_data)
238 {
239         E2kContext *ctx = user_data;
240         SoupAddress *addr;
241         struct sockaddr_in sin;
242         const char *local_ipaddr;
243         unsigned short port;
244         int s, ret;
245
246         ctx->priv->get_local_address_sock = NULL;
247
248         if (status != SOUP_STATUS_OK)
249                 goto done;
250
251         addr = soup_socket_get_local_address (sock);
252         local_ipaddr = soup_address_get_physical (addr);
253
254         s = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP);
255         if (SOCKET_IS_INVALID (s))
256                 goto done;
257
258         memset (&sin, 0, sizeof (sin));
259         sin.sin_family = AF_INET;
260
261         port = (short)getpid ();
262         do {
263                 port++;
264                 if (port < 1024)
265                         port += 1024;
266                 sin.sin_port = htons (port);
267                 ret = bind (s, (struct sockaddr *)&sin, sizeof (sin));
268         } while (STATUS_IS_SOCKET_ERROR (ret) && BIND_STATUS_IS_ADDRINUSE ());
269
270         if (ret == -1) {
271                 CLOSE_SOCKET (s);
272                 goto done;
273         }
274
275 #ifndef G_OS_WIN32
276         ctx->priv->listener_channel = g_io_channel_unix_new (s);
277 #else
278         ctx->priv->listener_channel = g_io_channel_win32_new_socket (s);
279 #endif
280         g_io_channel_set_encoding (ctx->priv->listener_channel, NULL, NULL);
281         g_io_channel_set_buffered (ctx->priv->listener_channel, FALSE);
282
283         ctx->priv->listener_watch_id =
284                 g_io_add_watch (ctx->priv->listener_channel,
285                                 G_IO_IN, do_notification, ctx);
286
287         ctx->priv->notification_uri = g_strdup_printf ("httpu://%s:%u/",
288                                                         local_ipaddr,
289                                                         port);
290
291         g_hash_table_foreach (ctx->priv->subscriptions_by_uri,
292                               renew_sub_list, ctx);
293
294  done:
295         if (sock)
296                 g_object_unref (sock);
297         g_object_unref (ctx);
298 }
299
300 /**
301  * e2k_context_new:
302  * @uri: OWA uri to connect to
303  *
304  * Creates a new #E2kContext based at @uri
305  *
306  * Return value: the new context
307  **/
308 E2kContext *
309 e2k_context_new (const char *uri)
310 {
311         E2kContext *ctx;
312         SoupUri *suri;
313
314         suri = soup_uri_new (uri);
315         if (!suri)
316                 return NULL;
317         
318         if (!suri->host) {
319                 soup_uri_free (suri);
320                 return NULL;
321         }
322
323         ctx = g_object_new (E2K_TYPE_CONTEXT, NULL);
324         ctx->priv->owa_uri = g_strdup (uri);
325
326         g_object_ref (ctx);
327         ctx->priv->get_local_address_sock =
328                 soup_socket_client_new_async (
329                         suri->host, suri->port, NULL,
330                         got_connection, ctx);
331         soup_uri_free (suri);
332
333         return ctx;
334 }
335
336 static void
337 session_authenticate (SoupSession *session, SoupMessage *msg,
338                       const char *auth_type, const char *auth_realm,
339                       char **username, char **password, gpointer user_data)
340 {
341         E2kContext *ctx = user_data;
342
343         *username = g_strdup (ctx->priv->username);
344         *password = g_strdup (ctx->priv->password);
345 }
346
347 /**
348  * e2k_context_set_auth:
349  * @ctx: the context
350  * @username: the Windows username (not including domain) of the user
351  * @domain: the NT domain, or %NULL to use the default (if using NTLM)
352  * @authmech: the HTTP Authorization type to use; either "Basic" or "NTLM"
353  * @password: the user's password
354  *
355  * Sets the authentication information on @ctx. This will have the
356  * side effect of cancelling any pending requests on @ctx.
357  **/
358 void
359 e2k_context_set_auth (E2kContext *ctx, const char *username,
360                       const char *domain, const char *authmech,
361                       const char *password)
362 {
363         guint timeout = E2K_SOUP_SESSION_TIMEOUT;
364
365         g_return_if_fail (E2K_IS_CONTEXT (ctx));
366
367         if (username) {
368                 g_free (ctx->priv->username);
369                 if (domain) {
370                         ctx->priv->username =
371                                 g_strdup_printf ("%s\\%s", domain,
372                                                  username);
373                 } else
374                         ctx->priv->username = g_strdup (username);
375         }
376
377         if (password) {
378                 g_free (ctx->priv->password);
379                 ctx->priv->password = g_strdup (password);
380         }
381
382         /* Destroy the old sessions so we don't reuse old auths */
383         if (ctx->priv->session)
384                 g_object_unref (ctx->priv->session);
385         if (ctx->priv->async_session)
386                 g_object_unref (ctx->priv->async_session);
387
388         /* Set a default timeout value of 30 seconds.
389            FIXME: Make timeout configurable 
390         */
391         if (g_getenv ("SOUP_SESSION_TIMEOUT"))
392                 timeout = atoi (g_getenv ("SOUP_SESSION_TIMEOUT"));
393         
394         ctx->priv->session = soup_session_sync_new_with_options (
395                 SOUP_SESSION_USE_NTLM, !authmech || !strcmp (authmech, "NTLM"),
396                 SOUP_SESSION_TIMEOUT, timeout,
397                 NULL);
398         g_signal_connect (ctx->priv->session, "authenticate",
399                           G_CALLBACK (session_authenticate), ctx);
400         soup_session_add_filter (ctx->priv->session,
401                                  SOUP_MESSAGE_FILTER (ctx));
402
403         ctx->priv->async_session = soup_session_async_new_with_options (
404                 SOUP_SESSION_USE_NTLM, !authmech || !strcmp (authmech, "NTLM"),
405                 NULL);
406         g_signal_connect (ctx->priv->async_session, "authenticate",
407                           G_CALLBACK (session_authenticate), ctx);
408         soup_session_add_filter (ctx->priv->async_session,
409                                  SOUP_MESSAGE_FILTER (ctx));
410 }
411
412 /**
413  * e2k_context_get_last_timestamp:
414  * @ctx: the context
415  *
416  * Returns a %time_t corresponding to the last "Date" header
417  * received from the server.
418  *
419  * Return value: the timestamp
420  **/
421 time_t
422 e2k_context_get_last_timestamp (E2kContext *ctx)
423 {
424         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), -1);
425
426         return ctx->priv->last_timestamp;
427 }
428
429 #ifdef E2K_DEBUG
430 /* Debug levels:
431  * 0 - None
432  * 1 - Basic request and response
433  * 2 - 1 plus all headers
434  * 3 - 2 plus all bodies
435  * 4 - 3 plus Global Catalog debug too
436  */
437
438 static void
439 print_header (gpointer name, gpointer value, gpointer data)
440 {
441         printf ("%s: %s\n", (char *)name, (char *)value);
442 }
443
444 static void
445 e2k_debug_print_request (SoupMessage *msg, const char *note)
446 {
447         const SoupUri *uri;
448
449         uri = soup_message_get_uri (msg);
450         printf ("%s %s%s%s HTTP/1.1\nE2k-Debug: %p @ %lu",
451                 msg->method, uri->path,
452                 uri->query ? "?" : "",
453                 uri->query ? uri->query : "",
454                 msg, (unsigned long)time (NULL));
455         if (note)
456                 printf (" [%s]\n", note);
457         else
458                 printf ("\n");
459         if (e2k_debug_level > 1) {
460                 print_header ("Host", uri->host, NULL);
461                 soup_message_foreach_header (msg->request_headers,
462                                              print_header, NULL);
463         }
464         if (e2k_debug_level > 2 && msg->request.length &&
465             strcmp (msg->method, "POST")) {
466                 printf ("\n");
467                 fwrite (msg->request.body, 1, msg->request.length, stdout);
468                 if (msg->request.body[msg->request.length - 1] != '\n')
469                         printf ("\n");
470         }
471         printf ("\n");
472 }
473
474 static void
475 e2k_debug_print_response (SoupMessage *msg)
476 {
477         printf ("%d %s\nE2k-Debug: %p @ %lu\n",
478                 msg->status_code, msg->reason_phrase,
479                 msg, time (NULL));
480         if (e2k_debug_level > 1) {
481                 soup_message_foreach_header (msg->response_headers,
482                                              print_header, NULL);
483         }
484         if (e2k_debug_level > 2 && msg->response.length &&
485             E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
486                 const char *content_type =
487                         soup_message_get_header (msg->response_headers,
488                                                  "Content-Type");
489                 if (!content_type || e2k_debug_level > 4 ||
490                     g_ascii_strcasecmp (content_type, "text/html")) {
491                         printf ("\n");
492                         fwrite (msg->response.body, 1, msg->response.length, stdout);
493                         if (msg->response.body[msg->response.length - 1] != '\n')
494                                 printf ("\n");
495                 }
496         }
497         printf ("\n");
498 }
499
500 static void
501 e2k_debug_handler (SoupMessage *msg, gpointer user_data)
502 {
503         gboolean restarted = GPOINTER_TO_INT (user_data);
504
505         e2k_debug_print_response (msg);
506         if (restarted)
507                 e2k_debug_print_request (msg, "restarted");
508 }
509
510 static void
511 e2k_debug_setup (SoupMessage *msg)
512 {
513         if (!e2k_debug_level)
514                 return;
515
516         e2k_debug_print_request (msg, NULL);
517
518         g_signal_connect (msg, "finished",
519                           G_CALLBACK (e2k_debug_handler),
520                           GINT_TO_POINTER (FALSE));
521         g_signal_connect (msg, "restarted",
522                           G_CALLBACK (e2k_debug_handler),
523                           GINT_TO_POINTER (TRUE));
524 }
525 #endif
526
527 #define E2K_FBA_FLAG_FORCE_DOWNLEVEL 1
528 #define E2K_FBA_FLAG_TRUSTED         4
529
530 /**
531  * e2k_context_fba:
532  * @ctx: the context
533  * @failed_msg: a message that received a 440 status code
534  *
535  * Attempts to synchronously perform Exchange 2003 forms-based
536  * authentication.
537  *
538  * Return value: %FALSE if authentication failed, %TRUE if it
539  * succeeded, in which case @failed_msg can be requeued.
540  **/
541 gboolean
542 e2k_context_fba (E2kContext *ctx, SoupMessage *failed_msg)
543 {
544         static gboolean in_fba_auth = FALSE;
545         int status, len;
546         char *body = NULL;
547         char *action, *method, *name, *value;
548         xmlDoc *doc = NULL;
549         xmlNode *node;
550         SoupMessage *post_msg;
551         GString *form_body, *cookie_str;
552         const GSList *cookies, *c;
553
554         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), FALSE);
555
556         if (in_fba_auth)
557                 return FALSE;
558
559         if (ctx->priv->cookie) {
560                 g_free (ctx->priv->cookie);
561                 ctx->priv->cookie = NULL;
562                 if (!ctx->priv->cookie_verified) {
563                         /* New cookie failed on the first try. Must
564                          * be a bad password.
565                          */
566                         return FALSE;
567                 }
568                 /* Otherwise, it's just expired. */
569         }
570
571         if (!ctx->priv->username || !ctx->priv->password)
572                 return FALSE;
573
574         in_fba_auth = TRUE;
575
576         status = e2k_context_get_owa (ctx, NULL, ctx->priv->owa_uri,
577                                       FALSE, &body, &len);
578         if (!SOUP_STATUS_IS_SUCCESSFUL (status) || len == 0)
579                 goto failed;
580
581         doc = e2k_parse_html (body, len);
582         g_free (body);
583
584         node = e2k_xml_find (doc->children, "form");
585         if (!node)
586                 goto failed;
587
588         method = xmlGetProp (node, "method");
589         if (!method || g_ascii_strcasecmp (method, "post") != 0) {
590                 if (method)
591                         xmlFree (method);
592                 goto failed;
593         }
594         xmlFree (method);
595
596         value = xmlGetProp (node, "action");
597         if (!value || !*value)
598                 goto failed;
599         if (*value == '/') {
600                 SoupUri *suri;
601
602                 suri = soup_uri_new (ctx->priv->owa_uri);
603                 g_free (suri->path);
604                 suri->path = g_strdup (value);
605                 action = soup_uri_to_string (suri, FALSE);
606                 soup_uri_decode (action);
607                 soup_uri_free (suri);
608         } else
609                 action = g_strdup (value);
610         xmlFree (value);
611
612         form_body = g_string_new (NULL);
613         while ((node = e2k_xml_find (node, "input"))) {
614                 name = xmlGetProp (node, "name");
615                 if (!name)
616                         continue;
617                 value = xmlGetProp (node, "value");
618
619                 if (!g_ascii_strcasecmp (name, "destination") && value) {
620                         g_string_append (form_body, name);
621                         g_string_append_c (form_body, '=');
622                         e2k_uri_append_encoded (form_body, value, FALSE, NULL);
623                         g_string_append_c (form_body, '&');
624                 } else if (!g_ascii_strcasecmp (name, "flags")) {
625                         g_string_append_printf (form_body, "flags=%d",
626                                                 E2K_FBA_FLAG_TRUSTED);
627                         g_string_append_c (form_body, '&');
628                 } else if (!g_ascii_strcasecmp (name, "username")) {
629                         g_string_append (form_body, "username=");
630                         e2k_uri_append_encoded (form_body, ctx->priv->username, FALSE, NULL);
631                         g_string_append_c (form_body, '&');
632                 } else if (!g_ascii_strcasecmp (name, "password")) {
633                         g_string_append (form_body, "password=");
634                         e2k_uri_append_encoded (form_body, ctx->priv->password, FALSE, NULL);
635                         g_string_append_c (form_body, '&');
636                 }
637
638                 if (value)
639                         xmlFree (value);
640                 xmlFree (name);
641         }
642         g_string_append_printf (form_body, "trusted=%d", E2K_FBA_FLAG_TRUSTED);
643         xmlFreeDoc (doc);
644         doc = NULL;
645
646         post_msg = e2k_soup_message_new_full (ctx, action, "POST",
647                                               "application/x-www-form-urlencoded",
648                                               SOUP_BUFFER_SYSTEM_OWNED,
649                                               form_body->str, form_body->len);
650         soup_message_set_flags (post_msg, SOUP_MESSAGE_NO_REDIRECT);
651         e2k_context_send_message (ctx, NULL /* FIXME? */, post_msg);
652         g_string_free (form_body, FALSE);
653         g_free (action);
654
655         if (!SOUP_STATUS_IS_SUCCESSFUL (post_msg->status_code) &&
656             !SOUP_STATUS_IS_REDIRECTION (post_msg->status_code)) {
657                 g_object_unref (post_msg);
658                 goto failed;
659         }
660
661         /* Extract the cookies */
662         cookies = soup_message_get_header_list (post_msg->response_headers,
663                                                 "Set-Cookie");
664         cookie_str = g_string_new (NULL);
665
666         for (c = cookies; c; c = c->next) {
667                 value = c->data;
668                 len = strcspn (value, ";");
669
670                 if (cookie_str->len)
671                         g_string_append (cookie_str, "; ");
672                 g_string_append_len (cookie_str, value, len);
673         }
674         ctx->priv->cookie = cookie_str->str;
675         ctx->priv->cookie_verified = FALSE;
676         g_string_free (cookie_str, FALSE);
677         g_object_unref (post_msg);
678
679         in_fba_auth = FALSE;
680
681         /* Set up the failed message to be requeued */
682         soup_message_remove_header (failed_msg->request_headers, "Cookie");
683         soup_message_add_header (failed_msg->request_headers,
684                                  "Cookie", ctx->priv->cookie);
685         return TRUE;
686
687  failed:
688         in_fba_auth = FALSE;
689         if (doc)
690                 xmlFreeDoc (doc);
691         return FALSE;
692 }
693
694 static void
695 fba_timeout_handler (SoupMessage *msg, gpointer user_data)
696 {
697         E2kContext *ctx = user_data;
698
699 #ifdef E2K_DEBUG
700         if (e2k_debug_level)
701                 e2k_debug_print_response (msg);
702 #endif
703
704         if (e2k_context_fba (ctx, msg))
705                 soup_session_requeue_message (ctx->priv->session, msg);
706         else
707                 soup_message_set_status (msg, SOUP_STATUS_UNAUTHORIZED);
708 }
709
710 static void
711 timestamp_handler (SoupMessage *msg, gpointer user_data)
712 {
713         E2kContext *ctx = user_data;
714         const char *date;
715
716         date = soup_message_get_header (msg->response_headers, "Date");
717         if (date)
718                 ctx->priv->last_timestamp = e2k_http_parse_date (date);
719 }
720
721 static void
722 redirect_handler (SoupMessage *msg, gpointer user_data)
723 {
724         E2kContext *ctx = user_data;
725         const char *new_uri;
726         SoupUri *soup_uri;
727         char *old_uri;
728
729         if (soup_message_get_flags (msg) & SOUP_MESSAGE_NO_REDIRECT)
730                 return;
731
732         new_uri = soup_message_get_header (msg->response_headers, "Location");
733         if (new_uri) {
734                 soup_uri = soup_uri_copy (soup_message_get_uri (msg));
735                 old_uri = soup_uri_to_string (soup_uri, FALSE);
736
737                 g_signal_emit (ctx, signals[REDIRECT], 0,
738                                msg->status_code, old_uri, new_uri);
739                 soup_uri_free (soup_uri);
740                 g_free (old_uri);
741         }
742 }
743
744 static void
745 setup_message (SoupMessageFilter *filter, SoupMessage *msg)
746 {
747         E2kContext *ctx = E2K_CONTEXT (filter);
748
749
750         if (ctx->priv->cookie) {
751                 soup_message_remove_header (msg->request_headers, "Cookie");
752                 soup_message_add_header (msg->request_headers,
753                                          "Cookie", ctx->priv->cookie);
754         }
755
756         /* Only do this the first time through */
757         if (!soup_message_get_header (msg->request_headers, "User-Agent")) {
758                 soup_message_add_handler (msg, SOUP_HANDLER_PRE_BODY,
759                                           timestamp_handler, ctx);
760                 soup_message_add_status_class_handler (msg, SOUP_STATUS_CLASS_REDIRECT,
761                                                        SOUP_HANDLER_PRE_BODY,
762                                                        redirect_handler, ctx);
763                 soup_message_add_status_code_handler (msg, E2K_HTTP_TIMEOUT,
764                                                       SOUP_HANDLER_PRE_BODY,
765                                                       fba_timeout_handler, ctx);
766                 soup_message_add_header (msg->request_headers, "User-Agent",
767                                          "Evolution/" VERSION);
768
769 #ifdef E2K_DEBUG
770                 e2k_debug_setup (msg);
771 #endif
772         }
773 }
774
775 /**
776  * e2k_soup_message_new:
777  * @ctx: the context
778  * @uri: the URI
779  * @method: the HTTP method
780  *
781  * Creates a new %SoupMessage for @ctx.
782  *
783  * Return value: a new %SoupMessage, set up for connector use
784  **/
785 SoupMessage *
786 e2k_soup_message_new (E2kContext *ctx, const char *uri, const char *method)
787 {
788         SoupMessage *msg;
789
790         if (method[0] == 'B') {
791                 char *slash_uri = e2k_strdup_with_trailing_slash (uri);
792                 msg = soup_message_new (method, slash_uri);
793                 g_free (slash_uri);
794         } else
795                 msg = soup_message_new (method, uri);
796
797         return msg;
798 }
799
800 /**
801  * e2k_soup_message_new_full:
802  * @ctx: the context
803  * @uri: the URI
804  * @method: the HTTP method
805  * @content_type: MIME Content-Type of @body
806  * @owner: ownership of @body
807  * @body: request body
808  * @length: length of @body
809  *
810  * Creates a new %SoupMessage with the given body.
811  *
812  * Return value: a new %SoupMessage with a request body, set up for
813  * connector use
814  **/
815 SoupMessage *
816 e2k_soup_message_new_full (E2kContext *ctx, const char *uri,
817                            const char *method, const char *content_type,
818                            SoupOwnership owner, const char *body,
819                            gulong length)
820 {
821         SoupMessage *msg;
822
823         msg = e2k_soup_message_new (ctx, uri, method);
824         soup_message_set_request (msg, content_type,
825                                   owner, (char *)body, length);
826
827         return msg;
828 }
829
830 /**
831  * e2k_context_queue_message:
832  * @ctx: the context
833  * @msg: the message to queue
834  * @callback: callback to invoke when @msg is done
835  * @user_data: data for @callback
836  *
837  * Asynchronously queues @msg in @ctx's session.
838  **/
839 void
840 e2k_context_queue_message (E2kContext *ctx, SoupMessage *msg,
841                            SoupMessageCallbackFn callback,
842                            gpointer user_data)
843 {
844         g_return_if_fail (E2K_IS_CONTEXT (ctx));
845
846         soup_session_queue_message (ctx->priv->async_session, msg,
847                                     callback, user_data);
848 }
849
850 static void
851 context_canceller (E2kOperation *op, gpointer owner, gpointer data)
852 {
853         E2kContext *ctx = owner;
854         SoupMessage *msg = data;
855
856         soup_message_set_status (msg, SOUP_STATUS_CANCELLED);
857         soup_session_cancel_message (ctx->priv->session, msg);
858 }
859
860 /**
861  * e2k_context_send_message:
862  * @ctx: the context
863  * @op: an #E2kOperation to use for cancellation
864  * @msg: the message to send
865  *
866  * Synchronously sends @msg in @ctx's session.
867  *
868  * Return value: the HTTP status of the message
869  **/
870 E2kHTTPStatus
871 e2k_context_send_message (E2kContext *ctx, E2kOperation *op, SoupMessage *msg)
872 {
873         E2kHTTPStatus status;
874
875         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
876
877         if (e2k_operation_is_cancelled (op)) {
878                 soup_message_set_status (msg, E2K_HTTP_CANCELLED);
879                 return E2K_HTTP_CANCELLED;
880         }
881
882         e2k_operation_start (op, context_canceller, ctx, msg);
883         status = soup_session_send_message (ctx->priv->session, msg);
884         e2k_operation_finish (op);
885
886         return status;
887 }
888
889
890 static void
891 update_unique_uri (E2kContext *ctx, SoupMessage *msg,
892                    const char *folder_uri, const char *encoded_name, int *count,
893                    E2kContextTestCallback test_callback, gpointer user_data)
894 {
895         SoupUri *suri;
896         char *uri = NULL;
897
898         do {
899                 g_free (uri);
900                 if (*count == 1) {
901                         uri = g_strdup_printf ("%s%s.EML", folder_uri,
902                                                encoded_name);
903                 } else {
904                         uri = g_strdup_printf ("%s%s-%d.EML", folder_uri,
905                                                encoded_name, *count);
906                 }
907                 (*count)++;
908         } while (test_callback && !test_callback (ctx, uri, user_data));
909
910         suri = soup_uri_new (uri);
911         soup_message_set_uri (msg, suri);
912         soup_uri_free (suri);
913         g_free (uri);
914 }
915
916
917 /* GET */
918
919 static SoupMessage *
920 get_msg (E2kContext *ctx, const char *uri, gboolean owa, gboolean claim_ie)
921 {
922         SoupMessage *msg;
923
924         msg = e2k_soup_message_new (ctx, uri, "GET");
925         if (!owa)
926                 soup_message_add_header (msg->request_headers, "Translate", "F");
927         if (claim_ie) {
928                 soup_message_remove_header (msg->request_headers, "User-Agent");
929                 soup_message_add_header (msg->request_headers, "User-Agent",
930                                          "MSIE 6.0b (Windows NT 5.0; compatible; "
931                                          "Evolution/" VERSION ")");
932         }
933
934         return msg;
935 }
936
937 /**
938  * e2k_context_get:
939  * @ctx: the context
940  * @op: pointer to an #E2kOperation to use for cancellation
941  * @uri: URI of the object to GET
942  * @content_type: if not %NULL, will contain the Content-Type of the
943  * response on return.
944  * @body: if not %NULL, will contain the response body on return
945  * @len: if not %NULL, will contain the response body length on return
946  *
947  * Performs a GET on @ctx for @uri. If successful (2xx status code),
948  * the Content-Type, body and length will be returned. The body is not
949  * terminated by a '\0'. If the GET is not successful, @content_type,
950  * @body and @len will be untouched (even if the error response
951  * included a body).
952  *
953  * Return value: the HTTP status
954  **/
955 E2kHTTPStatus
956 e2k_context_get (E2kContext *ctx, E2kOperation *op, const char *uri,
957                  char **content_type, char **body, int *len)
958 {
959         SoupMessage *msg;
960         E2kHTTPStatus status;
961
962         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
963         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
964
965         msg = get_msg (ctx, uri, FALSE, FALSE);
966         status = e2k_context_send_message (ctx, op, msg);
967
968         if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status)) {
969                 if (content_type) {
970                         const char *header;
971                         header = soup_message_get_header (msg->response_headers,
972                                                           "Content-Type");
973                         *content_type = g_strdup (header);
974                 }
975                 if (body) {
976                         *body = msg->response.body;
977                         msg->response.owner = SOUP_BUFFER_USER_OWNED;
978                 }
979                 if (len)
980                         *len = msg->response.length;
981         }
982
983         g_object_unref (msg);
984         return status;
985 }
986
987 /**
988  * e2k_context_get_owa:
989  * @ctx: the context
990  * @op: pointer to an #E2kOperation to use for cancellation
991  * @uri: URI of the object to GET
992  * @claim_ie: whether or not to claim to be IE
993  * @body: if not %NULL, will contain the response body on return
994  * @len: if not %NULL, will contain the response body length on return
995  *
996  * As with e2k_context_get(), but used when you need the HTML or XML
997  * data that would be returned to OWA rather than the raw object data.
998  *
999  * Return value: the HTTP status
1000  **/
1001 E2kHTTPStatus
1002 e2k_context_get_owa (E2kContext *ctx, E2kOperation *op,
1003                      const char *uri, gboolean claim_ie,
1004                      char **body, int *len)
1005 {
1006         SoupMessage *msg;
1007         E2kHTTPStatus status;
1008
1009         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1010         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1011
1012         msg = get_msg (ctx, uri, TRUE, claim_ie);
1013         status = e2k_context_send_message (ctx, op, msg);
1014
1015         if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status)) {
1016                 if (body) {
1017                         *body = msg->response.body;
1018                         msg->response.owner = SOUP_BUFFER_USER_OWNED;
1019                 }
1020                 if (len)
1021                         *len = msg->response.length;
1022         }
1023
1024         g_object_unref (msg);
1025         return status;
1026 }
1027
1028 /* PUT / POST */
1029
1030 static SoupMessage *
1031 put_msg (E2kContext *ctx, const char *uri, const char *content_type,
1032          SoupOwnership buffer_type, const char *body, int length)
1033 {
1034         SoupMessage *msg;
1035
1036         msg = e2k_soup_message_new_full (ctx, uri, "PUT", content_type,
1037                                          buffer_type, body, length);
1038         soup_message_add_header (msg->request_headers, "Translate", "f");
1039
1040         return msg;
1041 }
1042
1043 static SoupMessage *
1044 post_msg (E2kContext *ctx, const char *uri, const char *content_type,
1045           SoupOwnership buffer_type, const char *body, int length)
1046 {
1047         SoupMessage *msg;
1048
1049         msg = e2k_soup_message_new_full (ctx, uri, "POST", content_type,
1050                                          buffer_type, body, length);
1051         soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
1052
1053         return msg;
1054 }
1055
1056 static void
1057 extract_put_results (SoupMessage *msg, char **location, char **repl_uid)
1058 {
1059         const char *header;
1060
1061         if (!E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code))
1062                 return;
1063
1064         if (repl_uid) {
1065                 header = soup_message_get_header (msg->response_headers,
1066                                                   "Repl-UID");
1067                 *repl_uid = g_strdup (header);
1068         }
1069         if (location) {
1070                 header = soup_message_get_header (msg->response_headers,
1071                                                   "Location");
1072                 *location = g_strdup (header);
1073         }
1074 }
1075
1076 /**
1077  * e2k_context_put:
1078  * @ctx: the context
1079  * @op: pointer to an #E2kOperation to use for cancellation
1080  * @uri: the URI to PUT to
1081  * @content_type: MIME Content-Type of the data
1082  * @body: data to PUT
1083  * @length: length of @body
1084  * @repl_uid: if not %NULL, will contain the Repl-UID of the PUT
1085  * object on return
1086  *
1087  * Performs a PUT operation on @ctx for @uri.
1088  *
1089  * Return value: the HTTP status
1090  **/
1091 E2kHTTPStatus
1092 e2k_context_put (E2kContext *ctx, E2kOperation *op, const char *uri,
1093                  const char *content_type, const char *body, int length,
1094                  char **repl_uid)
1095 {
1096         SoupMessage *msg;
1097         E2kHTTPStatus status;
1098
1099         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1100         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1101         g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1102         g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1103
1104         msg = put_msg (ctx, uri, content_type,
1105                        SOUP_BUFFER_USER_OWNED,
1106                        body, length);
1107         status = e2k_context_send_message (ctx, op, msg);
1108         extract_put_results (msg, NULL, repl_uid);
1109
1110         g_object_unref (msg);
1111         return status;
1112 }
1113
1114 /**
1115  * e2k_context_put_new:
1116  * @ctx: the context
1117  * @op: pointer to an #E2kOperation to use for cancellation
1118  * @folder_uri: the URI of the folder to PUT into
1119  * @object_name: base name of the new object (not URI-encoded)
1120  * @test_callback: callback to use to test possible object URIs
1121  * @user_data: data for @test_callback
1122  * @content_type: MIME Content-Type of the data
1123  * @body: data to PUT
1124  * @length: length of @body
1125  * @location: if not %NULL, will contain the Location of the PUT
1126  * object on return
1127  * @repl_uid: if not %NULL, will contain the Repl-UID of the PUT
1128  * object on return
1129  *
1130  * PUTs data into @folder_uri on @ctx with a new name based on
1131  * @object_name. If @test_callback is non-%NULL, it will be called
1132  * with each URI that is considered for the object so that the caller
1133  * can check its summary data to see if that URI is in use
1134  * (potentially saving one or more round-trips to the server).
1135  *
1136  * Return value: the HTTP status
1137  **/
1138 E2kHTTPStatus
1139 e2k_context_put_new (E2kContext *ctx, E2kOperation *op,
1140                      const char *folder_uri, const char *object_name,
1141                      E2kContextTestCallback test_callback, gpointer user_data,
1142                      const char *content_type, const char *body, int length,
1143                      char **location, char **repl_uid)
1144 {
1145         SoupMessage *msg;
1146         E2kHTTPStatus status;
1147         char *slash_uri, *encoded_name;
1148         int count;
1149
1150         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1151         g_return_val_if_fail (folder_uri != NULL, E2K_HTTP_MALFORMED);
1152         g_return_val_if_fail (object_name != NULL, E2K_HTTP_MALFORMED);
1153         g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1154         g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1155
1156         slash_uri = e2k_strdup_with_trailing_slash (folder_uri);
1157         encoded_name = e2k_uri_encode (object_name, TRUE, NULL);
1158
1159         /* folder_uri is a dummy here */
1160         msg = put_msg (ctx, folder_uri, content_type,
1161                        SOUP_BUFFER_USER_OWNED, body, length);
1162         soup_message_add_header (msg->request_headers, "If-None-Match", "*");
1163
1164         count = 1;
1165         do {
1166                 update_unique_uri (ctx, msg, slash_uri, encoded_name, &count,
1167                                    test_callback, user_data);
1168                 status = e2k_context_send_message (ctx, op, msg);
1169         } while (status == E2K_HTTP_PRECONDITION_FAILED);
1170
1171         extract_put_results (msg, location, repl_uid);
1172
1173         g_object_unref (msg);
1174         g_free (slash_uri);
1175         g_free (encoded_name);
1176         return status;
1177 }
1178
1179 /**
1180  * e2k_context_post:
1181  * @ctx: the context
1182  * @op: pointer to an #E2kOperation to use for cancellation
1183  * @uri: the URI to POST to
1184  * @content_type: MIME Content-Type of the data
1185  * @body: data to PUT
1186  * @length: length of @body
1187  * @location: if not %NULL, will contain the Location of the POSTed
1188  * object on return
1189  * @repl_uid: if not %NULL, will contain the Repl-UID of the POSTed
1190  * object on return
1191  *
1192  * Performs a POST operation on @ctx for @uri.
1193  *
1194  * Note that POSTed objects will be irrevocably(?) marked as "unsent",
1195  * If you open a POSTed message in Outlook, it will open in the
1196  * composer rather than in the message viewer.
1197  *
1198  * Return value: the HTTP status
1199  **/
1200 E2kHTTPStatus
1201 e2k_context_post (E2kContext *ctx, E2kOperation *op, const char *uri,
1202                   const char *content_type, const char *body, int length,
1203                   char **location, char **repl_uid)
1204 {
1205         SoupMessage *msg;
1206         E2kHTTPStatus status;
1207
1208         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1209         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1210         g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1211         g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1212
1213         msg = post_msg (ctx, uri, content_type,
1214                         SOUP_BUFFER_USER_OWNED,
1215                         body, length);
1216
1217         status = e2k_context_send_message (ctx, op, msg);
1218         extract_put_results (msg, location, repl_uid);
1219
1220         g_object_unref (msg);
1221         return status;
1222 }
1223
1224 /* PROPPATCH */
1225
1226 static void
1227 add_namespaces (const char *namespace, char abbrev, gpointer user_data)
1228 {
1229         GString *propxml = user_data;
1230
1231         g_string_append_printf (propxml, " xmlns:%c=\"%s\"", abbrev, namespace);
1232 }
1233
1234 static void
1235 write_prop (GString *xml, const char *propertyname,
1236             E2kPropType type, gpointer value, gboolean set)
1237 {
1238         const char *namespace, *name, *typestr;
1239         char *encoded, abbrev;
1240         gboolean b64enc, need_type;
1241         GByteArray *data;
1242         GPtrArray *array;
1243         int i;
1244
1245         if (set && (value == NULL))
1246                 return;
1247
1248         namespace = e2k_prop_namespace_name (propertyname);
1249         abbrev = e2k_prop_namespace_abbrev (propertyname);
1250         name = e2k_prop_property_name (propertyname);
1251
1252         g_string_append_printf (xml, "<%c:%s", abbrev, name);
1253         if (!set) {
1254                 /* This means we are removing the property, so just return
1255                    with ending tag */
1256                 g_string_append (xml, "/>");
1257                 return;
1258         }
1259
1260         need_type = (strstr (namespace, "/mapi/id/") != NULL);
1261         if (!need_type)
1262                 g_string_append_c (xml, '>');
1263
1264         switch (type) {
1265         case E2K_PROP_TYPE_BINARY:
1266                 if (need_type)
1267                         g_string_append (xml, " T:dt=\"bin.base64\">");
1268                 data = value;
1269                 encoded = e2k_base64_encode (data->data, data->len);
1270                 g_string_append (xml, encoded);
1271                 g_free (encoded);
1272                 break;
1273
1274         case E2K_PROP_TYPE_STRING_ARRAY:
1275                 typestr = " T:dt=\"mv.string\">";
1276                 b64enc = FALSE;
1277                 goto array_common;
1278
1279         case E2K_PROP_TYPE_INT_ARRAY:
1280                 typestr = " T:dt=\"mv.int\">";
1281                 b64enc = FALSE;
1282                 goto array_common;
1283
1284         case E2K_PROP_TYPE_BINARY_ARRAY:
1285                 typestr = " T:dt=\"mv.bin.base64\">";
1286                 b64enc = TRUE;
1287
1288         array_common:
1289                 if (need_type)
1290                         g_string_append (xml, typestr);
1291                 array = value;
1292                 for (i = 0; i < array->len; i++) {
1293                         g_string_append (xml, "<X:v>");
1294
1295                         if (b64enc) {
1296                                 data = array->pdata[i];
1297                                 encoded = e2k_base64_encode (data->data,
1298                                                              data->len);
1299                                 g_string_append (xml, encoded);
1300                                 g_free (encoded);
1301                         } else
1302                                 e2k_g_string_append_xml_escaped (xml, array->pdata[i]);
1303
1304                         g_string_append (xml, "</X:v>");
1305                 }
1306                 break;
1307
1308         case E2K_PROP_TYPE_XML:
1309                 g_assert_not_reached ();
1310                 break;
1311
1312         case E2K_PROP_TYPE_STRING:
1313         default:
1314                 if (need_type) {
1315                         switch (type) {
1316                         case E2K_PROP_TYPE_INT:
1317                                 typestr = " T:dt=\"int\">";
1318                                 break;
1319                         case E2K_PROP_TYPE_BOOL:
1320                                 typestr = " T:dt=\"boolean\">";
1321                                 break;
1322                         case E2K_PROP_TYPE_FLOAT:
1323                                 typestr = " T:dt=\"float\">";
1324                                 break;
1325                         case E2K_PROP_TYPE_DATE:
1326                                 typestr = " T:dt=\"dateTime.tz\">";
1327                                 break;
1328                         default:
1329                                 typestr = ">";
1330                                 break;
1331                         }
1332                         g_string_append (xml, typestr);
1333                 }
1334                 e2k_g_string_append_xml_escaped (xml, value);
1335                 break;
1336
1337         }
1338
1339         g_string_append_printf (xml, "</%c:%s>", abbrev, name);
1340 }
1341
1342 static void
1343 add_set_props (const char *propertyname, E2kPropType type,
1344                gpointer value, gpointer user_data)
1345 {
1346         GString **props = user_data;
1347
1348         if (!*props)
1349                 *props = g_string_new (NULL);
1350
1351         write_prop (*props, propertyname, type, value, TRUE);
1352 }
1353
1354 static void
1355 add_remove_props (const char *propertyname, E2kPropType type,
1356                   gpointer value, gpointer user_data)
1357 {
1358         GString **props = user_data;
1359
1360         if (!*props)
1361                 *props = g_string_new (NULL);
1362
1363         write_prop (*props, propertyname, type, value, FALSE);
1364 }
1365
1366 static SoupMessage *
1367 patch_msg (E2kContext *ctx, const char *uri, const char *method,
1368            const char **hrefs, int nhrefs, E2kProperties *props,
1369            gboolean create)
1370 {
1371         SoupMessage *msg;
1372         GString *propxml, *subxml;
1373         int i;
1374
1375         propxml = g_string_new (E2K_XML_HEADER);
1376         g_string_append (propxml, "<D:propertyupdate xmlns:D=\"DAV:\"");
1377
1378         /* Iterate over the properties, noting each namespace once,
1379          * then add them all to the header.
1380          */
1381         e2k_properties_foreach_namespace (props, add_namespaces, propxml);
1382         g_string_append (propxml, ">\r\n");
1383
1384         /* If this is a BPROPPATCH, add the <target> section. */
1385         if (hrefs) {
1386                 g_string_append (propxml, "<D:target>\r\n");
1387                 for (i = 0; i < nhrefs; i++) {
1388                         g_string_append_printf (propxml, "<D:href>%s</D:href>",
1389                                                 hrefs[i]);
1390                 }
1391                 g_string_append (propxml, "\r\n</D:target>\r\n");
1392         }
1393
1394         /* Add <set> properties. */
1395         subxml = NULL;
1396         e2k_properties_foreach (props, add_set_props, &subxml);
1397         if (subxml) {
1398                 g_string_append (propxml, "<D:set><D:prop>\r\n");
1399                 g_string_append (propxml, subxml->str);
1400                 g_string_append (propxml, "\r\n</D:prop></D:set>");
1401                 g_string_free (subxml, TRUE);
1402         }
1403
1404         /* Add <remove> properties. */
1405         subxml = NULL;
1406         e2k_properties_foreach_removed (props, add_remove_props, &subxml);
1407         if (subxml) {
1408                 g_string_append (propxml, "<D:remove><D:prop>\r\n");
1409                 g_string_append (propxml, subxml->str);
1410                 g_string_append (propxml, "\r\n</D:prop></D:remove>");
1411                 g_string_free (subxml, TRUE);
1412         }
1413
1414         /* Finish it up */
1415         g_string_append (propxml, "\r\n</D:propertyupdate>");
1416
1417         /* And build the message. */
1418         msg = e2k_soup_message_new_full (ctx, uri, method,
1419                                          "text/xml", SOUP_BUFFER_SYSTEM_OWNED,
1420                                          propxml->str, propxml->len);
1421         g_string_free (propxml, FALSE);
1422         soup_message_add_header (msg->request_headers, "Brief", "t");
1423         if (!create)
1424                 soup_message_add_header (msg->request_headers, "If-Match", "*");
1425
1426         return msg;
1427 }
1428
1429 /**
1430  * e2k_context_proppatch:
1431  * @ctx: the context
1432  * @op: pointer to an #E2kOperation to use for cancellation
1433  * @uri: the URI to PROPPATCH
1434  * @props: the properties to set/remove
1435  * @create: whether or not to create @uri if it does not exist
1436  * @repl_uid: if not %NULL, will contain the Repl-UID of the
1437  * PROPPATCHed object on return
1438  *
1439  * Performs a PROPPATCH operation on @ctx for @uri.
1440  *
1441  * If @create is %FALSE and @uri does not already exist, the response
1442  * code will be %E2K_HTTP_PRECONDITION_FAILED.
1443  *
1444  * Return value: the HTTP status
1445  **/
1446 E2kHTTPStatus
1447 e2k_context_proppatch (E2kContext *ctx, E2kOperation *op,
1448                        const char *uri, E2kProperties *props,
1449                        gboolean create, char **repl_uid)
1450 {
1451         SoupMessage *msg;
1452         E2kHTTPStatus status;
1453
1454         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1455         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1456         g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1457
1458         msg = patch_msg (ctx, uri, "PROPPATCH", NULL, 0, props, create);
1459         status = e2k_context_send_message (ctx, op, msg);
1460         extract_put_results (msg, NULL, repl_uid);
1461
1462         g_object_unref (msg);
1463         return status;
1464 }
1465
1466 /**
1467  * e2k_context_proppatch_new:
1468  * @ctx: the context
1469  * @op: pointer to an #E2kOperation to use for cancellation
1470  * @folder_uri: the URI of the folder to PROPPATCH a new object in
1471  * @object_name: base name of the new object (not URI-encoded)
1472  * @test_callback: callback to use to test possible object URIs
1473  * @user_data: data for @test_callback
1474  * @props: the properties to set/remove
1475  * @location: if not %NULL, will contain the Location of the
1476  * PROPPATCHed object on return
1477  * @repl_uid: if not %NULL, will contain the Repl-UID of the
1478  * PROPPATCHed object on return
1479  *
1480  * PROPPATCHes data into @folder_uri on @ctx with a new name based on
1481  * @object_name. If @test_callback is non-%NULL, it will be called
1482  * with each URI that is considered for the object so that the caller
1483  * can check its summary data to see if that URI is in use
1484  * (potentially saving one or more round-trips to the server).
1485
1486  * Return value: the HTTP status
1487  **/
1488 E2kHTTPStatus
1489 e2k_context_proppatch_new (E2kContext *ctx, E2kOperation *op,
1490                            const char *folder_uri, const char *object_name,
1491                            E2kContextTestCallback test_callback,
1492                            gpointer user_data,
1493                            E2kProperties *props,
1494                            char **location, char **repl_uid)
1495 {
1496         SoupMessage *msg;
1497         E2kHTTPStatus status;
1498         char *slash_uri, *encoded_name;
1499         int count;
1500
1501         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1502         g_return_val_if_fail (folder_uri != NULL, E2K_HTTP_MALFORMED);
1503         g_return_val_if_fail (object_name != NULL, E2K_HTTP_MALFORMED);
1504         g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1505
1506         slash_uri = e2k_strdup_with_trailing_slash (folder_uri);
1507         encoded_name = e2k_uri_encode (object_name, TRUE, NULL);
1508
1509         /* folder_uri is a dummy here */
1510         msg = patch_msg (ctx, folder_uri, "PROPPATCH", NULL, 0, props, TRUE);
1511         soup_message_add_header (msg->request_headers, "If-None-Match", "*");
1512
1513         count = 1;
1514         do {
1515                 update_unique_uri (ctx, msg, slash_uri, encoded_name, &count,
1516                                    test_callback, user_data);
1517                 status = e2k_context_send_message (ctx, op, msg);
1518         } while (status == E2K_HTTP_PRECONDITION_FAILED);
1519
1520         if (location)
1521                 *location = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
1522         extract_put_results (msg, NULL, repl_uid);
1523
1524         g_object_unref (msg);
1525         g_free (slash_uri);
1526         g_free (encoded_name);
1527         return status;
1528 }
1529
1530 static E2kHTTPStatus
1531 bproppatch_fetch (E2kResultIter *iter,
1532                   E2kContext *ctx, E2kOperation *op,
1533                   E2kResult **results, int *nresults,
1534                   int *first, int *total,
1535                   gpointer user_data)
1536 {
1537         SoupMessage *msg = user_data;
1538         E2kHTTPStatus status;
1539
1540         if (msg->status != SOUP_MESSAGE_STATUS_IDLE)
1541                 return E2K_HTTP_OK;
1542
1543         status = e2k_context_send_message (ctx, op, msg);
1544         if (status == E2K_HTTP_MULTI_STATUS) {
1545                 e2k_results_from_multistatus (msg, results, nresults);
1546                 *total = *nresults;
1547         }
1548         return status;
1549 }
1550
1551 static void
1552 bproppatch_free (E2kResultIter *iter, gpointer msg)
1553 {
1554         g_object_unref (msg);
1555 }
1556
1557 /**
1558  * e2k_context_bproppatch_start:
1559  * @ctx: the context
1560  * @op: pointer to an #E2kOperation to use for cancellation
1561  * @uri: the base URI
1562  * @hrefs: array of URIs, possibly relative to @uri
1563  * @nhrefs: length of @hrefs
1564  * @props: the properties to set/remove
1565  * @create: whether or not to create @uri if it does not exist
1566  *
1567  * Begins a BPROPPATCH (bulk PROPPATCH) of @hrefs based at @uri.
1568  *
1569  * Return value: an iterator for getting the results of the BPROPPATCH
1570  **/
1571 E2kResultIter *
1572 e2k_context_bproppatch_start (E2kContext *ctx, E2kOperation *op,
1573                               const char *uri, const char **hrefs, int nhrefs,
1574                               E2kProperties *props, gboolean create)
1575 {
1576         SoupMessage *msg;
1577
1578         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1579         g_return_val_if_fail (uri != NULL, NULL);
1580         g_return_val_if_fail (props != NULL, NULL);
1581
1582         msg = patch_msg (ctx, uri, "BPROPPATCH", hrefs, nhrefs, props, create);
1583         return e2k_result_iter_new (ctx, op, TRUE, -1,
1584                                     bproppatch_fetch, bproppatch_free,
1585                                     msg);
1586 }
1587
1588 /* PROPFIND */
1589
1590 static SoupMessage *
1591 propfind_msg (E2kContext *ctx, const char *base_uri,
1592               const char **props, int nprops, const char **hrefs, int nhrefs)
1593 {
1594         SoupMessage *msg;
1595         GString *propxml;
1596         GData *set_namespaces;
1597         const char *name;
1598         char abbrev;
1599         int i;
1600
1601         propxml = g_string_new (E2K_XML_HEADER);
1602         g_string_append (propxml, "<D:propfind xmlns:D=\"DAV:\"");
1603
1604         set_namespaces = NULL;
1605         for (i = 0; i < nprops; i++) {
1606                 name = e2k_prop_namespace_name (props[i]);
1607                 abbrev = e2k_prop_namespace_abbrev (props[i]);
1608
1609                 if (!g_datalist_get_data (&set_namespaces, name)) {
1610                         g_datalist_set_data (&set_namespaces, name,
1611                                              GINT_TO_POINTER (1));
1612                         g_string_append_printf (propxml, " xmlns:%c=\"%s\"",
1613                                                 abbrev, name);
1614                 }
1615         }
1616         g_datalist_clear (&set_namespaces);
1617         g_string_append (propxml, ">\r\n");
1618
1619         if (hrefs) {
1620                 g_string_append (propxml, "<D:target>\r\n");
1621                 for (i = 0; i < nhrefs; i++) {
1622                         g_string_append_printf (propxml, "<D:href>%s</D:href>",
1623                                                 hrefs[i]);
1624                 }
1625                 g_string_append (propxml, "\r\n</D:target>\r\n");
1626         }
1627
1628         g_string_append (propxml, "<D:prop>\r\n");
1629         for (i = 0; i < nprops; i++) {
1630                 abbrev = e2k_prop_namespace_abbrev (props[i]);
1631                 name = e2k_prop_property_name (props[i]);
1632                 g_string_append_printf (propxml, "<%c:%s/>", abbrev, name);
1633         }
1634         g_string_append (propxml, "\r\n</D:prop>\r\n</D:propfind>");
1635
1636         msg = e2k_soup_message_new_full (ctx, base_uri, 
1637                                          hrefs ? "BPROPFIND" : "PROPFIND",
1638                                          "text/xml", SOUP_BUFFER_SYSTEM_OWNED,
1639                                          propxml->str, propxml->len);
1640         g_string_free (propxml, FALSE);
1641         soup_message_add_header (msg->request_headers, "Brief", "t");
1642         soup_message_add_header (msg->request_headers, "Depth", "0");
1643
1644         return msg;
1645 }
1646
1647 /**
1648  * e2k_context_propfind:
1649  * @ctx: the context
1650  * @op: pointer to an #E2kOperation to use for cancellation
1651  * @uri: the URI to PROPFIND on
1652  * @props: array of properties to find
1653  * @nprops: length of @props
1654  * @results: on return, the results
1655  * @nresults: length of @results
1656  *
1657  * Performs a PROPFIND operation on @ctx for @uri. If successful, the
1658  * results are returned as an array of #E2kResult (which you must free
1659  * with e2k_results_free()), but the array will always have either 0
1660  * or 1 members.
1661  *
1662  * Return value: the HTTP status
1663  **/
1664 E2kHTTPStatus
1665 e2k_context_propfind (E2kContext *ctx, E2kOperation *op,
1666                       const char *uri, const char **props, int nprops,
1667                       E2kResult **results, int *nresults)
1668 {
1669         SoupMessage *msg;
1670         E2kHTTPStatus status;
1671
1672         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1673         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1674         g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1675
1676         msg = propfind_msg (ctx, uri, props, nprops, NULL, 0);
1677         status = e2k_context_send_message (ctx, op, msg);
1678
1679         if (msg->status_code == E2K_HTTP_MULTI_STATUS)
1680                 e2k_results_from_multistatus (msg, results, nresults);
1681         g_object_unref (msg);
1682         return status;
1683 }
1684
1685 static E2kHTTPStatus
1686 bpropfind_fetch (E2kResultIter *iter,
1687                  E2kContext *ctx, E2kOperation *op,
1688                  E2kResult **results, int *nresults,
1689                  int *first, int *total,
1690                  gpointer user_data)
1691 {
1692         GSList **msgs = user_data;
1693         E2kHTTPStatus status;
1694         SoupMessage *msg;
1695
1696         if (!*msgs)
1697                 return E2K_HTTP_OK;
1698
1699         msg = (*msgs)->data;
1700         *msgs = g_slist_remove (*msgs, msg);
1701
1702         status = e2k_context_send_message (ctx, op, msg);
1703         if (status == E2K_HTTP_MULTI_STATUS)
1704                 e2k_results_from_multistatus (msg, results, nresults);
1705         g_object_unref (msg);
1706
1707         return status;
1708 }
1709
1710 static void
1711 bpropfind_free (E2kResultIter *iter, gpointer user_data)
1712 {
1713         GSList **msgs = user_data, *m;
1714
1715         for (m = *msgs; m; m = m->next)
1716                 g_object_unref (m->data);
1717         g_slist_free (*msgs);
1718         g_free (msgs);
1719 }
1720
1721 /**
1722  * e2k_context_bpropfind_start:
1723  * @ctx: the context
1724  * @op: pointer to an #E2kOperation to use for cancellation
1725  * @uri: the base URI
1726  * @hrefs: array of URIs, possibly relative to @uri
1727  * @nhrefs: length of @hrefs
1728  * @props: array of properties to find
1729  * @nprops: length of @props
1730  *
1731  * Begins a BPROPFIND (bulk PROPFIND) operation on @ctx for @hrefs.
1732  *
1733  * Return value: an iterator for getting the results
1734  **/
1735 E2kResultIter *
1736 e2k_context_bpropfind_start (E2kContext *ctx, E2kOperation *op,
1737                              const char *uri, const char **hrefs, int nhrefs,
1738                              const char **props, int nprops)
1739 {
1740         SoupMessage *msg;
1741         GSList **msgs;
1742         int i;
1743
1744         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1745         g_return_val_if_fail (uri != NULL, NULL);
1746         g_return_val_if_fail (props != NULL, NULL);
1747         g_return_val_if_fail (hrefs != NULL, NULL);
1748
1749         msgs = g_new0 (GSList *, 1);
1750         for (i = 0; i < nhrefs; i += E2K_CONTEXT_MAX_BATCH_SIZE) {
1751                 msg = propfind_msg (ctx, uri, props, nprops,
1752                                     hrefs + i, MIN (E2K_CONTEXT_MAX_BATCH_SIZE, nhrefs - i));
1753                 *msgs = g_slist_append (*msgs, msg);
1754         }
1755
1756         return e2k_result_iter_new (ctx, op, TRUE, nhrefs,
1757                                     bpropfind_fetch, bpropfind_free,
1758                                     msgs);
1759 }
1760
1761 /* SEARCH */
1762
1763 static SoupMessage *
1764 search_msg (E2kContext *ctx, const char *uri,
1765             SoupOwnership buffer_type, const char *searchxml,
1766             int size, gboolean ascending, int offset)
1767 {
1768         SoupMessage *msg;
1769
1770         msg = e2k_soup_message_new_full (ctx, uri, "SEARCH", "text/xml",
1771                                          buffer_type, searchxml,
1772                                          strlen (searchxml));
1773         soup_message_add_header (msg->request_headers, "Brief", "t");
1774
1775         if (size) {
1776                 char *range;
1777
1778                 if (offset == INT_MAX) {
1779                         range = g_strdup_printf ("rows=-%u", size);
1780                 } else {
1781                         range = g_strdup_printf ("rows=%u-%u",
1782                                                  offset, offset + size - 1);
1783                 }
1784                 soup_message_add_header (msg->request_headers, "Range", range);
1785                 g_free (range);
1786         }
1787
1788         return msg;
1789 }
1790
1791 static char *
1792 search_xml (const char **props, int nprops,
1793             E2kRestriction *rn, const char *orderby)
1794 {
1795         GString *xml;
1796         char *ret, *where;
1797         int i;
1798
1799         xml = g_string_new (E2K_XML_HEADER);
1800         g_string_append (xml, "<searchrequest xmlns=\"DAV:\"><sql>\r\n");
1801         g_string_append (xml, "SELECT ");
1802
1803         for (i = 0; i < nprops; i++) {
1804                 if (i > 0)
1805                         g_string_append (xml, ", ");
1806                 g_string_append_c (xml, '"');
1807                 g_string_append   (xml, props[i]);
1808                 g_string_append_c (xml, '"');
1809         }
1810
1811         if (e2k_restriction_folders_only (rn))
1812                 g_string_append_printf (xml, "\r\nFROM SCOPE('hierarchical traversal of \"\"')\r\n");
1813         else
1814                 g_string_append (xml, "\r\nFROM \"\"\r\n");
1815
1816         if (rn) {
1817                 where = e2k_restriction_to_sql (rn);
1818                 if (where) {
1819                         e2k_g_string_append_xml_escaped (xml, where);
1820                         g_string_append (xml, "\r\n");
1821                         g_free (where);
1822                 }
1823         }
1824
1825         if (orderby)
1826                 g_string_append_printf (xml, "ORDER BY \"%s\"\r\n", orderby);
1827
1828         g_string_append (xml, "</sql></searchrequest>");
1829
1830         ret = xml->str;
1831         g_string_free (xml, FALSE);
1832
1833         return ret;
1834 }
1835
1836 static gboolean
1837 search_result_get_range (SoupMessage *msg, int *first, int *total)
1838 {
1839         const char *range, *p;
1840
1841         range = soup_message_get_header (msg->response_headers,
1842                                          "Content-Range");
1843         if (!range)
1844                 return FALSE;
1845         p = strstr (range, "rows ");
1846         if (!p)
1847                 return FALSE;
1848
1849         if (first)
1850                 *first = atoi (p + 5);
1851
1852         if (total) {
1853                 p = strstr (range, "total=");
1854                 if (p)
1855                         *total = atoi (p + 6);
1856                 else
1857                         *total = -1;
1858         }
1859
1860         return TRUE;
1861 }
1862
1863 typedef struct {
1864         char *uri, *xml;
1865         gboolean ascending;
1866         int batch_size, next;
1867 } E2kSearchData;
1868
1869 static E2kHTTPStatus
1870 search_fetch (E2kResultIter *iter,
1871               E2kContext *ctx, E2kOperation *op,
1872               E2kResult **results, int *nresults,
1873               int *first, int *total,
1874               gpointer user_data)
1875 {
1876         E2kSearchData *search_data = user_data;
1877         E2kHTTPStatus status;
1878         SoupMessage *msg;
1879
1880         if (search_data->batch_size == 0)
1881                 return E2K_HTTP_OK;
1882
1883         msg = search_msg (ctx, search_data->uri,
1884                           SOUP_BUFFER_USER_OWNED, search_data->xml,
1885                           search_data->batch_size,
1886                           search_data->ascending, search_data->next);
1887         status = e2k_context_send_message (ctx, op, msg);
1888         if (msg->status_code == E2K_HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
1889                 status = E2K_HTTP_OK;
1890         else if (status == E2K_HTTP_MULTI_STATUS) {
1891                 search_result_get_range (msg, first, total);
1892                 if (*total == 0)
1893                         goto cleanup;
1894
1895                 e2k_results_from_multistatus (msg, results, nresults);
1896                 if (*total == -1)
1897                         *total = *first + *nresults;
1898
1899                 if (search_data->ascending && *first + *nresults < *total)
1900                         search_data->next = *first + *nresults;
1901                 else if (!search_data->ascending && *first > 0) {
1902                         if (*first >= search_data->batch_size)
1903                                 search_data->next = *first - search_data->batch_size;
1904                         else {
1905                                 search_data->batch_size = *first;
1906                                 search_data->next = 0;
1907                         }
1908                 } else
1909                         search_data->batch_size = 0;
1910         }
1911
1912  cleanup:
1913         g_object_unref (msg);
1914         return status;
1915 }
1916
1917 static void
1918 search_free (E2kResultIter *iter, gpointer user_data)
1919 {
1920         E2kSearchData *search_data = user_data;
1921
1922         g_free (search_data->uri);
1923         g_free (search_data->xml);
1924         g_free (search_data);
1925 }
1926
1927 /**
1928  * e2k_context_search_start:
1929  * @ctx: the context
1930  * @op: pointer to an #E2kOperation to use for cancellation
1931  * @uri: the folder to search
1932  * @props: the properties to search for
1933  * @nprops: size of @props array
1934  * @rn: the search restriction
1935  * @orderby: if non-%NULL, the field to sort the search results by
1936  * @ascending: %TRUE for an ascending search, %FALSE for descending.
1937  *
1938  * Begins a SEARCH on @ctx at @uri.
1939  *
1940  * Return value: an iterator for returning the search results
1941  **/
1942 E2kResultIter *
1943 e2k_context_search_start (E2kContext *ctx, E2kOperation *op, const char *uri,
1944                           const char **props, int nprops, E2kRestriction *rn,
1945                           const char *orderby, gboolean ascending)
1946 {
1947         E2kSearchData *search_data;
1948
1949         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1950         g_return_val_if_fail (uri != NULL, NULL);
1951         g_return_val_if_fail (props != NULL, NULL);
1952
1953         search_data = g_new0 (E2kSearchData, 1);
1954         search_data->uri = g_strdup (uri);
1955         search_data->xml = search_xml (props, nprops, rn, orderby);
1956         search_data->ascending = ascending;
1957         search_data->batch_size = E2K_CONTEXT_MAX_BATCH_SIZE;
1958         search_data->next = ascending ? 0 : INT_MAX;
1959
1960         return e2k_result_iter_new (ctx, op, ascending, -1,
1961                                     search_fetch, search_free,
1962                                     search_data);
1963 }
1964
1965
1966
1967 /* DELETE */
1968
1969 static SoupMessage *
1970 delete_msg (E2kContext *ctx, const char *uri)
1971 {
1972         return e2k_soup_message_new (ctx, uri, "DELETE");
1973 }
1974
1975 /**
1976  * e2k_context_delete:
1977  * @ctx: the context
1978  * @op: pointer to an #E2kOperation to use for cancellation
1979  * @uri: URI to DELETE
1980  *
1981  * Attempts to DELETE @uri on @ctx.
1982  *
1983  * Return value: the HTTP status
1984  **/
1985 E2kHTTPStatus
1986 e2k_context_delete (E2kContext *ctx, E2kOperation *op, const char *uri)
1987 {
1988         SoupMessage *msg;
1989         E2kHTTPStatus status;
1990
1991         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1992         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1993
1994         msg = delete_msg (ctx, uri);
1995         status = e2k_context_send_message (ctx, op, msg);
1996
1997         g_object_unref (msg);
1998         return status;
1999 }
2000
2001 /* BDELETE */
2002
2003 static SoupMessage *
2004 bdelete_msg (E2kContext *ctx, const char *uri, const char **hrefs, int nhrefs)
2005 {
2006         SoupMessage *msg;
2007         GString *xml;
2008         int i;
2009
2010         xml = g_string_new (E2K_XML_HEADER "<delete xmlns=\"DAV:\"><target>");
2011
2012         for (i = 0; i < nhrefs; i++) {
2013                 g_string_append (xml, "<href>");
2014                 e2k_g_string_append_xml_escaped (xml, hrefs[i]);
2015                 g_string_append (xml, "</href>");
2016         }
2017
2018         g_string_append (xml, "</target></delete>");
2019
2020         msg = e2k_soup_message_new_full (ctx, uri, "BDELETE", "text/xml",
2021                                          SOUP_BUFFER_SYSTEM_OWNED,
2022                                          xml->str, xml->len);
2023         g_string_free (xml, FALSE);
2024
2025         return msg;
2026 }
2027
2028 static E2kHTTPStatus
2029 bdelete_fetch (E2kResultIter *iter,
2030                E2kContext *ctx, E2kOperation *op,
2031                E2kResult **results, int *nresults,
2032                int *first, int *total,
2033                gpointer user_data)
2034 {
2035         GSList **msgs = user_data;
2036         E2kHTTPStatus status;
2037         SoupMessage *msg;
2038
2039         if (!*msgs)
2040                 return E2K_HTTP_OK;
2041
2042         msg = (*msgs)->data;
2043         *msgs = g_slist_remove (*msgs, msg);
2044
2045         status = e2k_context_send_message (ctx, op, msg);
2046         if (status == E2K_HTTP_MULTI_STATUS)
2047                 e2k_results_from_multistatus (msg, results, nresults);
2048         g_object_unref (msg);
2049
2050         return status;
2051 }
2052
2053 static void
2054 bdelete_free (E2kResultIter *iter, gpointer user_data)
2055 {
2056         GSList **msgs = user_data, *m;
2057
2058         for (m = (*msgs); m; m = m->next)
2059                 g_object_unref (m->data);
2060         g_slist_free (*msgs);
2061         g_free (msgs);
2062 }
2063
2064 /**
2065  * e2k_context_bdelete_start:
2066  * @ctx: the context
2067  * @op: pointer to an #E2kOperation to use for cancellation
2068  * @uri: the base URI
2069  * @hrefs: array of URIs, possibly relative to @uri, to delete
2070  * @nhrefs: length of @hrefs
2071  *
2072  * Begins a BDELETE (bulk DELETE) operation on @ctx for @hrefs.
2073  *
2074  * Return value: an iterator for returning the results
2075  **/
2076 E2kResultIter *
2077 e2k_context_bdelete_start (E2kContext *ctx, E2kOperation *op,
2078                            const char *uri, const char **hrefs, int nhrefs)
2079 {
2080         GSList **msgs;
2081         int i, batchsize;
2082         SoupMessage *msg;
2083
2084         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
2085         g_return_val_if_fail (uri != NULL, NULL);
2086         g_return_val_if_fail (hrefs != NULL, NULL);
2087
2088         batchsize = (nhrefs + 9) / 10;
2089         if (batchsize < E2K_CONTEXT_MIN_BATCH_SIZE)
2090                 batchsize = E2K_CONTEXT_MIN_BATCH_SIZE;
2091         else if (batchsize > E2K_CONTEXT_MAX_BATCH_SIZE)
2092                 batchsize = E2K_CONTEXT_MAX_BATCH_SIZE;
2093
2094         msgs = g_new0 (GSList *, 1);
2095         for (i = 0; i < nhrefs; i += batchsize) {
2096                 batchsize = MIN (batchsize, nhrefs - i);
2097                 msg = bdelete_msg (ctx, uri, hrefs + i, batchsize);
2098                 *msgs = g_slist_prepend (*msgs, msg);
2099         }
2100
2101         return e2k_result_iter_new (ctx, op, TRUE, nhrefs,
2102                                     bdelete_fetch, bdelete_free,
2103                                     msgs);
2104 }
2105
2106 /* MKCOL */
2107
2108 /**
2109  * e2k_context_mkcol:
2110  * @ctx: the context
2111  * @op: pointer to an #E2kOperation to use for cancellation
2112  * @uri: URI of the new folder
2113  * @props: properties to set on the new folder, or %NULL
2114  * @permanent_url: if not %NULL, will contain the permanent URL of the
2115  * new folder on return
2116  *
2117  * Performs a MKCOL operation on @ctx to create @uri, with optional
2118  * additional properties.
2119  *
2120  * Return value: the HTTP status
2121  **/
2122 E2kHTTPStatus
2123 e2k_context_mkcol (E2kContext *ctx, E2kOperation *op,
2124                    const char *uri, E2kProperties *props,
2125                    char **permanent_url)
2126 {
2127         SoupMessage *msg;
2128         E2kHTTPStatus status;
2129
2130         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
2131         g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
2132
2133         if (!props)
2134                 msg = e2k_soup_message_new (ctx, uri, "MKCOL");
2135         else
2136                 msg = patch_msg (ctx, uri, "MKCOL", NULL, 0, props, TRUE);
2137
2138         status = e2k_context_send_message (ctx, op, msg);
2139         if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status) && permanent_url) {
2140                 const char *header;
2141
2142                 header = soup_message_get_header (msg->response_headers,
2143                                                   "MS-Exchange-Permanent-URL");
2144                 *permanent_url = g_strdup (header);
2145         }
2146
2147         g_object_unref (msg);
2148         return status;
2149 }
2150
2151 /* BMOVE / BCOPY */
2152
2153 static SoupMessage *
2154 transfer_msg (E2kContext *ctx,
2155               const char *source_uri, const char *dest_uri,
2156               const char **source_hrefs, int nhrefs,
2157               gboolean delete_originals)
2158 {
2159         SoupMessage *msg;
2160         GString *xml;
2161         int i;
2162
2163         xml = g_string_new (E2K_XML_HEADER);
2164         g_string_append (xml, delete_originals ? "<move" : "<copy");
2165         g_string_append (xml, " xmlns=\"DAV:\"><target>");
2166         for (i = 0; i < nhrefs; i++) {
2167                 g_string_append (xml, "<href>");
2168                 e2k_g_string_append_xml_escaped (xml, source_hrefs[i]);
2169                 g_string_append (xml, "</href>");
2170         }
2171         g_string_append (xml, "</target></");
2172         g_string_append (xml, delete_originals ? "move>" : "copy>");
2173
2174         msg = e2k_soup_message_new_full (ctx, source_uri,
2175                                          delete_originals ? "BMOVE" : "BCOPY",
2176                                          "text/xml",
2177                                          SOUP_BUFFER_SYSTEM_OWNED,
2178                                          xml->str, xml->len);
2179         soup_message_add_header (msg->request_headers, "Overwrite", "f");
2180         soup_message_add_header (msg->request_headers, "Allow-Rename", "t");
2181         soup_message_add_header (msg->request_headers, "Destination", dest_uri);
2182         g_string_free (xml, FALSE);
2183
2184         return msg;
2185 }
2186
2187 static E2kHTTPStatus
2188 transfer_next (E2kResultIter *iter,
2189                E2kContext *ctx, E2kOperation *op,
2190                E2kResult **results, int *nresults,
2191                int *first, int *total,
2192                gpointer user_data)
2193 {
2194         GSList **msgs = user_data;
2195         SoupMessage *msg;
2196         E2kHTTPStatus status;
2197
2198         if (!*msgs)
2199                 return E2K_HTTP_OK;
2200
2201         msg = (*msgs)->data;
2202         *msgs = g_slist_remove (*msgs, msg);
2203
2204         status = e2k_context_send_message (ctx, op, msg);
2205         if (status == E2K_HTTP_MULTI_STATUS)
2206                 e2k_results_from_multistatus (msg, results, nresults);
2207
2208         g_object_unref (msg);
2209         return status;
2210 }
2211
2212 static void
2213 transfer_free (E2kResultIter *iter, gpointer user_data)
2214 {
2215         GSList **msgs = user_data, *m;
2216
2217         for (m = *msgs; m; m = m->next)
2218                 g_object_unref (m->data);
2219         g_slist_free (*msgs);
2220         g_free (msgs);
2221 }
2222
2223 /**
2224  * e2k_context_transfer_start:
2225  * @ctx: the context
2226  * @op: pointer to an #E2kOperation to use for cancellation
2227  * @source_folder: URI of the source folder
2228  * @dest_folder: URI of the destination folder
2229  * @source_hrefs: an array of hrefs to move, relative to @source_folder
2230  * @delete_originals: whether or not to delete the original objects
2231  *
2232  * Starts a BMOVE or BCOPY (depending on @delete_originals) operation
2233  * on @ctx for @source_folder. The objects in @source_folder described
2234  * by @source_hrefs will be moved or copied to @dest_folder.
2235  * e2k_result_iter_next() can be used to check the success or failure
2236  * of each move/copy. (The #E2K_PR_DAV_LOCATION property for each
2237  * result will show the new location of the object.)
2238  *
2239  * NB: may not work correctly if @source_hrefs contains folders
2240  *
2241  * Return value: the iterator for the results
2242  **/
2243 E2kResultIter *
2244 e2k_context_transfer_start (E2kContext *ctx, E2kOperation *op,
2245                             const char *source_folder, const char *dest_folder,
2246                             GPtrArray *source_hrefs, gboolean delete_originals)
2247 {
2248         GSList **msgs;
2249         SoupMessage *msg;
2250         char *dest_uri;
2251         const char **hrefs;
2252         int i;
2253
2254         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
2255         g_return_val_if_fail (source_folder != NULL, NULL);
2256         g_return_val_if_fail (dest_folder != NULL, NULL);
2257         g_return_val_if_fail (source_hrefs != NULL, NULL);
2258
2259         dest_uri = e2k_strdup_with_trailing_slash (dest_folder);
2260         hrefs = (const char **)source_hrefs->pdata;
2261
2262         msgs = g_new0 (GSList *, 1);
2263         for (i = 0; i < source_hrefs->len; i += E2K_CONTEXT_MAX_BATCH_SIZE) {
2264                 msg = transfer_msg (ctx, source_folder, dest_uri,
2265                                     hrefs + i, MIN (E2K_CONTEXT_MAX_BATCH_SIZE, source_hrefs->len - i),
2266                                     delete_originals);
2267                 *msgs = g_slist_append (*msgs, msg);
2268         }
2269         g_free (dest_uri);
2270
2271         return e2k_result_iter_new (ctx, op, TRUE, source_hrefs->len,
2272                                     transfer_next, transfer_free,
2273                                     msgs);
2274 }
2275
2276 /**
2277  * e2k_context_transfer_dir:
2278  * @ctx: the context
2279  * @op: pointer to an #E2kOperation to use for cancellation
2280  * @source_href: URI of the source folder
2281  * @dest_href: URI of the destination folder
2282  * @delete_original: whether or not to delete the original folder
2283  * @permanent_url: if not %NULL, will contain the permanent URL of the
2284  * new folder on return
2285  *
2286  * Performs a MOVE or COPY (depending on @delete_original) operation
2287  * on @ctx for @source_href. The folder itself will be moved, renamed,
2288  * or copied to @dest_href (which is the name of the new folder
2289  * itself, not its parent).
2290  *
2291  * Return value: the HTTP status
2292  **/
2293 E2kHTTPStatus
2294 e2k_context_transfer_dir (E2kContext *ctx, E2kOperation *op,
2295                           const char *source_href, const char *dest_href,
2296                           gboolean delete_original,
2297                           char **permanent_url)
2298 {
2299         SoupMessage *msg;
2300         E2kHTTPStatus status;
2301
2302         g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
2303         g_return_val_if_fail (source_href != NULL, E2K_HTTP_MALFORMED);
2304         g_return_val_if_fail (dest_href != NULL, E2K_HTTP_MALFORMED);
2305
2306         msg = e2k_soup_message_new (ctx, source_href, delete_original ? "MOVE" : "COPY");
2307         soup_message_add_header (msg->request_headers, "Overwrite", "f");
2308         soup_message_add_header (msg->request_headers, "Destination", dest_href);
2309
2310         status = e2k_context_send_message (ctx, op, msg);
2311         if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status) && permanent_url) {
2312                 const char *header;
2313
2314                 header = soup_message_get_header (msg->response_headers,
2315                                                   "MS-Exchange-Permanent-URL");
2316                 *permanent_url = g_strdup (header);
2317         }
2318
2319         g_object_unref (msg);
2320         return status;
2321 }
2322
2323
2324 /* Subscriptions */
2325
2326 typedef struct {
2327         E2kContext *ctx;
2328         char *uri, *id;
2329         E2kContextChangeType type;
2330         int lifetime, min_interval;
2331         time_t last_notification;
2332
2333         E2kContextChangeCallback callback;
2334         gpointer user_data;
2335
2336         guint renew_timeout;
2337         SoupMessage *renew_msg;
2338         guint poll_timeout;
2339         SoupMessage *poll_msg;
2340         guint notification_timeout;
2341 } E2kSubscription;
2342
2343 static gboolean
2344 belated_notification (gpointer user_data)
2345 {
2346         E2kSubscription *sub = user_data;
2347
2348         sub->notification_timeout = 0;
2349         sub->callback (sub->ctx, sub->uri, sub->type, sub->user_data);
2350         return FALSE;
2351 }
2352
2353 static void
2354 maybe_notification (E2kSubscription *sub)
2355 {
2356         time_t now = time (NULL);
2357         int delay = sub->last_notification + sub->min_interval - now;
2358
2359         if (delay > 0) {
2360                 if (sub->notification_timeout)
2361                         g_source_remove (sub->notification_timeout);
2362                 sub->notification_timeout = g_timeout_add (delay * 1000,
2363                                                            belated_notification,
2364                                                            sub);
2365                 return;
2366         }
2367         sub->last_notification = now;
2368
2369         sub->callback (sub->ctx, sub->uri, sub->type, sub->user_data);
2370 }
2371
2372 static void
2373 polled (SoupMessage *msg, gpointer user_data)
2374 {
2375         E2kSubscription *sub = user_data;
2376         E2kContext *ctx = sub->ctx;
2377         E2kResult *results;
2378         int nresults, i;
2379         xmlNode *ids;
2380         char *id;
2381
2382         sub->poll_msg = NULL;
2383         if (msg->status_code != E2K_HTTP_MULTI_STATUS) {
2384                 g_warning ("Unexpected error %d %s from POLL",
2385                            msg->status_code, msg->reason_phrase);
2386                 return;
2387         }
2388
2389         e2k_results_from_multistatus (msg, &results, &nresults);
2390         for (i = 0; i < nresults; i++) {
2391                 if (results[i].status != E2K_HTTP_OK)
2392                         continue;
2393
2394                 ids = e2k_properties_get_prop (results[i].props, E2K_PR_SUBSCRIPTION_ID);
2395                 if (!ids)
2396                         continue;
2397                 for (ids = ids->xmlChildrenNode; ids; ids = ids->next) {
2398                         if (strcmp (ids->name, "li") != 0 ||
2399                             !ids->xmlChildrenNode ||
2400                             !ids->xmlChildrenNode->content)
2401                                 continue;
2402                         id = ids->xmlChildrenNode->content;
2403                         sub = g_hash_table_lookup (ctx->priv->subscriptions_by_id, id);
2404                         if (sub)
2405                                 maybe_notification (sub);
2406                 }
2407         }
2408         e2k_results_free (results, nresults);
2409 }
2410
2411 static gboolean
2412 timeout_notification (gpointer user_data)
2413 {
2414         E2kSubscription *sub = user_data, *sub2;
2415         E2kContext *ctx = sub->ctx;
2416         GList *sub_list;
2417         GString *subscription_ids;
2418
2419         sub->poll_timeout = 0;
2420         subscription_ids = g_string_new (sub->id);
2421
2422         /* Find all subscriptions at this URI that are awaiting a
2423          * POLL so we can POLL them all at once.
2424          */
2425         sub_list = g_hash_table_lookup (ctx->priv->subscriptions_by_uri,
2426                                         sub->uri);
2427         for (; sub_list; sub_list = sub_list->next) {
2428                 sub2 = sub_list->data;
2429                 if (sub2 == sub)
2430                         continue;
2431                 if (!sub2->poll_timeout)
2432                         continue;
2433                 g_source_remove (sub2->poll_timeout);
2434                 sub2->poll_timeout = 0;
2435                 g_string_append_printf (subscription_ids, ",%s", sub2->id);
2436         }
2437
2438         sub->poll_msg = e2k_soup_message_new (ctx, sub->uri, "POLL");
2439         soup_message_add_header (sub->poll_msg->request_headers,
2440                                  "Subscription-id", subscription_ids->str);
2441         e2k_context_queue_message (ctx, sub->poll_msg, polled, sub);
2442
2443         g_string_free (subscription_ids, TRUE);
2444         return FALSE;
2445 }
2446
2447 static gboolean
2448 do_notification (GIOChannel *source, GIOCondition condition, gpointer data)
2449 {
2450         E2kContext *ctx = data;
2451         E2kSubscription *sub;
2452         char buffer[1024], *id, *lasts;
2453         gsize len;
2454         GIOStatus status;
2455
2456         status = g_io_channel_read_chars (source, buffer, sizeof (buffer) - 1, &len, NULL);
2457         if (status != G_IO_STATUS_NORMAL && status != G_IO_STATUS_AGAIN) {
2458                 g_warning ("do_notification I/O error: %d (%s)", status,
2459                            g_strerror (errno));
2460                 return FALSE;
2461         }
2462         buffer[len] = '\0';
2463
2464 #ifdef E2K_DEBUG
2465         if (e2k_debug_level) {
2466                 if (e2k_debug_level == 1) {
2467                         fwrite (buffer, 1, strcspn (buffer, "\r\n"), stdout);
2468                         fputs ("\n\n", stdout);
2469                 } else
2470                         fputs (buffer, stdout);
2471         }
2472 #endif
2473
2474         if (g_ascii_strncasecmp (buffer, "NOTIFY ", 7) != 0)
2475                 return TRUE;
2476
2477         id = buffer;
2478         while (1) {
2479                 id = strchr (id, '\n');
2480                 if (!id++)
2481                         return TRUE;
2482                 if (g_ascii_strncasecmp (id, "Subscription-id: ", 17) == 0)
2483                         break;
2484         }
2485         id += 17;
2486
2487         for (id = strtok_r (id, ",\r", &lasts); id; id = strtok_r (NULL, ",\r", &lasts)) {
2488                 sub = g_hash_table_lookup (ctx->priv->subscriptions_by_id, id);
2489                 if (!sub)
2490                         continue;
2491
2492                 /* We don't want to POLL right away in case there are
2493                  * several changes in a row. So we just bump up the
2494                  * timeout to be one second from now. (Using an idle
2495                  * handler here doesn't actually work to prevent
2496                  * multiple POLLs.)
2497                  */
2498                 if (sub->poll_timeout)
2499                         g_source_remove (sub->poll_timeout);
2500                 sub->poll_timeout =
2501                         g_timeout_add (1000, timeout_notification, sub);
2502         }
2503
2504         return TRUE;
2505 }
2506
2507 static void
2508 renew_cb (SoupMessage *msg, gpointer user_data)
2509 {
2510         E2kSubscription *sub = user_data;
2511
2512         sub->renew_msg = NULL;
2513         if (!E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
2514                 g_warning ("renew_subscription: %d %s", msg->status_code,
2515                            msg->reason_phrase);
2516                 return;
2517         }
2518
2519         if (sub->id) {
2520                 g_hash_table_remove (sub->ctx->priv->subscriptions_by_id, sub->id);
2521                 g_free (sub->id);
2522         }
2523         sub->id = g_strdup (soup_message_get_header (msg->response_headers,
2524                                                      "Subscription-id"));
2525         g_return_if_fail (sub->id != NULL);
2526         g_hash_table_insert (sub->ctx->priv->subscriptions_by_id,
2527                              sub->id, sub);
2528 }
2529
2530 #define E2K_SUBSCRIPTION_INITIAL_LIFETIME  3600 /*  1 hour  */
2531 #define E2K_SUBSCRIPTION_MAX_LIFETIME     57600 /* 16 hours */
2532
2533 /* This must be kept in sync with E2kSubscriptionType */
2534 static char *subscription_type[] = {
2535         "update",               /* E2K_SUBSCRIPTION_OBJECT_CHANGED */
2536         "update/newmember",     /* E2K_SUBSCRIPTION_OBJECT_ADDED */
2537         "delete",               /* E2K_SUBSCRIPTION_OBJECT_REMOVED */
2538         "move"                  /* E2K_SUBSCRIPTION_OBJECT_MOVED */
2539 };
2540
2541 static gboolean
2542 renew_subscription (gpointer user_data)
2543 {
2544         E2kSubscription *sub = user_data;
2545         E2kContext *ctx = sub->ctx;
2546         char ltbuf[80];
2547
2548         if (!ctx->priv->notification_uri)
2549                 return FALSE;
2550
2551         if (sub->lifetime < E2K_SUBSCRIPTION_MAX_LIFETIME)
2552                 sub->lifetime *= 2;
2553
2554         sub->renew_msg = e2k_soup_message_new (ctx, sub->uri, "SUBSCRIBE");
2555         sprintf (ltbuf, "%d", sub->lifetime);
2556         soup_message_add_header (sub->renew_msg->request_headers,
2557                                  "Subscription-lifetime", ltbuf);
2558         soup_message_add_header (sub->renew_msg->request_headers,
2559                                  "Notification-type",
2560                                  subscription_type[sub->type]);
2561         if (sub->min_interval > 1) {
2562                 sprintf (ltbuf, "%d", sub->min_interval);
2563                 soup_message_add_header (sub->renew_msg->request_headers,
2564                                          "Notification-delay", ltbuf);
2565         }
2566         soup_message_add_header (sub->renew_msg->request_headers,
2567                                  "Call-back", ctx->priv->notification_uri);
2568
2569         e2k_context_queue_message (ctx, sub->renew_msg, renew_cb, sub);
2570         sub->renew_timeout = g_timeout_add ((sub->lifetime - 60) * 1000,
2571                                             renew_subscription, sub);
2572         return FALSE;
2573 }
2574
2575 /**
2576  * e2k_context_subscribe:
2577  * @ctx: the context
2578  * @uri: the folder URI to subscribe to notifications on
2579  * @type: the type of notification to subscribe to
2580  * @min_interval: the minimum interval (in seconds) between
2581  * notifications.
2582  * @callback: the callback to call when a notification has been
2583  * received
2584  * @user_data: data to pass to @callback.
2585  *
2586  * This subscribes to change notifications of the given @type on @uri.
2587  * @callback will (eventually) be invoked any time the folder changes
2588  * in the given way: whenever an object is added to it for
2589  * %E2K_CONTEXT_OBJECT_ADDED, whenever an object is deleted (but
2590  * not moved) from it (or the folder itself is deleted) for
2591  * %E2K_CONTEXT_OBJECT_REMOVED, whenever an object is moved in or
2592  * out of the folder for %E2K_CONTEXT_OBJECT_MOVED, and whenever
2593  * any of the above happens, or the folder or one of its items is
2594  * modified, for %E2K_CONTEXT_OBJECT_CHANGED. (This means that if
2595  * you subscribe to both CHANGED and some other notification on the
2596  * same folder that multiple callbacks may be invoked every time an
2597  * object is added/removed/moved/etc.)
2598  *
2599  * Notifications can be used *only* to discover changes made by other
2600  * clients! The code cannot assume that it will receive a notification
2601  * for every change that it makes to the server, for two reasons:
2602  * 
2603  * First, if multiple notifications occur within @min_interval seconds
2604  * of each other, the later ones will be suppressed, to avoid
2605  * excessive traffic between the client and the server as the client
2606  * tries to sync. Second, if there is a firewall between the client
2607  * and the server, it is possible that all notifications will be lost.
2608  **/
2609 void
2610 e2k_context_subscribe (E2kContext *ctx, const char *uri,
2611                        E2kContextChangeType type, int min_interval,
2612                        E2kContextChangeCallback callback,
2613                        gpointer user_data)
2614 {
2615         E2kSubscription *sub;
2616         GList *sub_list;
2617         gpointer key, value;
2618
2619         g_return_if_fail (E2K_IS_CONTEXT (ctx));
2620
2621         sub = g_new0 (E2kSubscription, 1);
2622         sub->ctx = ctx;
2623         sub->uri = g_strdup (uri);
2624         sub->type = type;
2625         sub->lifetime = E2K_SUBSCRIPTION_INITIAL_LIFETIME / 2;
2626         sub->min_interval = min_interval;
2627         sub->callback = callback;
2628         sub->user_data = user_data;
2629
2630         if (g_hash_table_lookup_extended (ctx->priv->subscriptions_by_uri,
2631                                           uri, &key, &value)) {
2632                 sub_list = value;
2633                 sub_list = g_list_prepend (sub_list, sub);
2634                 g_hash_table_insert (ctx->priv->subscriptions_by_uri,
2635                                      key, sub_list);
2636         } else {
2637                 g_hash_table_insert (ctx->priv->subscriptions_by_uri,
2638                                      sub->uri, g_list_prepend (NULL, sub));
2639         }
2640
2641         renew_subscription (sub);
2642 }
2643
2644 static void
2645 free_subscription (E2kSubscription *sub)
2646 {
2647         SoupSession *session = sub->ctx->priv->session;
2648
2649         if (sub->renew_timeout)
2650                 g_source_remove (sub->renew_timeout);
2651         if (sub->renew_msg)
2652                 soup_session_cancel_message (session, sub->renew_msg);
2653         if (sub->poll_timeout)
2654                 g_source_remove (sub->poll_timeout);
2655         if (sub->notification_timeout)
2656                 g_source_remove (sub->notification_timeout);
2657         if (sub->poll_msg)
2658                 soup_session_cancel_message (session, sub->poll_msg);
2659         g_free (sub->uri);
2660         g_free (sub->id);
2661         g_free (sub);
2662 }
2663
2664 static void
2665 unsubscribed (SoupMessage *msg, gpointer user_data)
2666 {
2667         ;
2668 }
2669
2670 static void
2671 unsubscribe_internal (E2kContext *ctx, const char *uri, GList *sub_list)
2672 {
2673         GList *l;
2674         E2kSubscription *sub;
2675         SoupMessage *msg;
2676         GString *subscription_ids = NULL;
2677
2678         for (l = sub_list; l; l = l->next) {
2679                 sub = l->data;
2680                 if (sub->id) {
2681                         if (!subscription_ids)
2682                                 subscription_ids = g_string_new (sub->id);
2683                         else {
2684                                 g_string_append_printf (subscription_ids,
2685                                                         ",%s", sub->id);
2686                         }
2687                         g_hash_table_remove (ctx->priv->subscriptions_by_id, sub->id);
2688                 }
2689                 free_subscription (sub);
2690         }
2691
2692         if (subscription_ids) {
2693                 msg = e2k_soup_message_new (ctx, uri, "UNSUBSCRIBE");
2694                 soup_message_add_header (msg->request_headers,
2695                                          "Subscription-id",
2696                                          subscription_ids->str);
2697                 e2k_context_queue_message (ctx, msg, unsubscribed, NULL);
2698                 g_string_free (subscription_ids, TRUE);
2699         }
2700 }
2701
2702 /**
2703  * e2k_context_unsubscribe:
2704  * @ctx: the context
2705  * @uri: the URI to unsubscribe from
2706  *
2707  * Unsubscribes to all notifications on @ctx for @uri.
2708  **/
2709 void
2710 e2k_context_unsubscribe (E2kContext *ctx, const char *uri)
2711 {
2712         GList *sub_list;
2713
2714         g_return_if_fail (E2K_IS_CONTEXT (ctx));
2715
2716         sub_list = g_hash_table_lookup (ctx->priv->subscriptions_by_uri, uri);
2717         g_hash_table_remove (ctx->priv->subscriptions_by_uri, uri);
2718         unsubscribe_internal (ctx, uri, sub_list);
2719         g_list_free (sub_list);
2720 }