[evolution-ews] I#89 - Calendar: Ability to create Online meeting



commit d850f84006bcd7c63e85f847d37f79886cac4488
Author: Milan Crha <mcrha redhat com>
Date:   Fri Jul 15 10:45:16 2022 +0200

    I#89 - Calendar: Ability to create Online meeting
    
    Closes https://gitlab.gnome.org/GNOME/evolution-ews/-/issues/89

 po/POTFILES.in                                     |   2 +
 src/EWS/calendar/CMakeLists.txt                    |  73 ++++++
 src/EWS/calendar/e-cal-backend-ews-m365.c          | 205 ++++++++++++++++
 src/EWS/calendar/e-cal-backend-ews-m365.h          |  27 ++
 src/EWS/calendar/e-cal-backend-ews.c               |  47 ++++
 src/EWS/evolution/CMakeLists.txt                   |   5 +
 src/EWS/evolution/e-ews-comp-editor-extension.c    | 273 +++++++++++++++++++++
 src/EWS/evolution/e-ews-comp-editor-extension.h    |  18 ++
 src/EWS/evolution/module-ews-configuration.c       |   2 +
 src/EWS/registry/CMakeLists.txt                    |   2 +
 src/EWS/registry/e-ews-backend.c                   | 122 +++++++--
 src/Microsoft365/common/e-m365-connection.c        |  32 ++-
 .../common/e-oauth2-service-microsoft365.c         |   3 +
 13 files changed, 787 insertions(+), 24 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e879e5b5..4f632eac 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -4,6 +4,7 @@
 org.gnome.Evolution-ews.metainfo.xml.in
 src/EWS/addressbook/e-book-backend-ews.c
 src/EWS/calendar/e-cal-backend-ews.c
+src/EWS/calendar/e-cal-backend-ews-m365.c
 src/EWS/calendar/e-cal-backend-ews-utils.c
 src/EWS/camel/camel-ews-folder.c
 src/EWS/camel/camel-ews-provider.c
@@ -19,6 +20,7 @@ src/EWS/common/e-ews-connection-utils.c
 src/EWS/common/e-ews-folder.c
 src/EWS/common/e-oauth2-service-office365.c
 src/EWS/evolution/e-book-config-ews.c
+src/EWS/evolution/e-ews-comp-editor-extension.c
 src/EWS/evolution/e-ews-config-lookup.c
 src/EWS/evolution/e-ews-config-utils.c
 src/EWS/evolution/e-ews-edit-folder-permissions.c
diff --git a/src/EWS/calendar/CMakeLists.txt b/src/EWS/calendar/CMakeLists.txt
index 3a6ce029..edb6efed 100644
--- a/src/EWS/calendar/CMakeLists.txt
+++ b/src/EWS/calendar/CMakeLists.txt
@@ -2,14 +2,83 @@ install(FILES windowsZones.xml
        DESTINATION ${ewsdatadir}
 )
 
+# helper static library
+
 set(DEPENDENCIES
        evolution-ews
 )
 
+add_library(ews-m365 STATIC
+       e-cal-backend-ews-m365.h
+       e-cal-backend-ews-m365.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/camel-m365-settings.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/camel-m365-settings.h
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-connection.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-connection.h
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-json-utils.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-json-utils.h
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-tz-utils.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/e-m365-tz-utils.h
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/calendar/e-cal-backend-m365-utils.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/calendar/e-cal-backend-m365-utils.h
+)
+
+add_dependencies(ews-m365
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(ews-m365 PRIVATE
+       -DG_LOG_DOMAIN=\"ecalbackendews-m365\"
+       -DM365_DATADIR=\"${ewsdatadir}\"
+)
+
+target_compile_options(ews-m365 PUBLIC
+       ${CAMEL_CFLAGS}
+       ${EVOLUTION_CALENDAR_CFLAGS}
+       ${LIBEBACKEND_CFLAGS}
+       ${LIBECAL_CFLAGS}
+       ${LIBEDATACAL_CFLAGS}
+       ${SOUP_CFLAGS}
+)
+
+target_include_directories(ews-m365 PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
+       ${CMAKE_BINARY_DIR}/src/Microsoft365
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365
+       ${CMAKE_CURRENT_BINARY_DIR}
+       ${CAMEL_INCLUDE_DIRS}
+       ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
+       ${LIBEBACKEND_INCLUDE_DIRS}
+       ${LIBECAL_INCLUDE_DIRS}
+       ${LIBEDATACAL_INCLUDE_DIRS}
+       ${SOUP_INCLUDE_DIRS}
+)
+
+target_link_libraries(ews-m365
+       ${DEPENDENCIES}
+       ${CAMEL_LDFLAGS}
+       ${EVOLUTION_CALENDAR_LDFLAGS}
+       ${LIBEBACKEND_LDFLAGS}
+       ${LIBECAL_LDFLAGS}
+       ${LIBEDATACAL_LDFLAGS}
+       ${SOUP_LDFLAGS}
+)
+
+# the main module
+
+set(DEPENDENCIES
+       evolution-ews
+       ews-m365
+)
+
 set(SOURCES
        e-cal-backend-ews.c
        e-cal-backend-ews.h
        e-cal-backend-ews-factory.c
+       e-cal-backend-ews-m365.h
        e-cal-backend-ews-utils.c
        e-cal-backend-ews-utils.h
 )
@@ -22,6 +91,10 @@ add_dependencies(ecalbackendews
        ${DEPENDENCIES}
 )
 
+set_target_properties(ecalbackendews PROPERTIES
+       C_VISIBILITY_PRESET hidden
+)
+
 target_compile_definitions(ecalbackendews PRIVATE
        -DG_LOG_DOMAIN=\"ecalbackendews\"
        -DEXCHANGE_EWS_DATADIR=\"${ewsdatadir}\"
diff --git a/src/EWS/calendar/e-cal-backend-ews-m365.c b/src/EWS/calendar/e-cal-backend-ews-m365.c
new file mode 100644
index 00000000..f1771ca8
--- /dev/null
+++ b/src/EWS/calendar/e-cal-backend-ews-m365.c
@@ -0,0 +1,205 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include <libedataserver/libedataserver.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+#include "common/e-m365-json-utils.h"
+#include "common/e-m365-tz-utils.h"
+#include "calendar/e-cal-backend-m365-utils.h"
+
+#include "e-cal-backend-ews-m365.h"
+
+static gboolean
+ecb_ews_m365_authenticate (EM365Connection *m365_cnc,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       ESourceAuthenticationResult auth_result;
+       ESourceCredentialsReason cred_reason;
+       gchar *certificate_pem = NULL;
+       GTlsCertificateFlags certificate_errors = 0;
+       ESource *source;
+       GError *local_error = NULL;
+
+       auth_result = e_m365_connection_authenticate_sync (m365_cnc, NULL, E_M365_FOLDER_KIND_CALENDAR,
+               NULL, NULL, &certificate_pem, &certificate_errors, cancellable, &local_error);
+
+       if (auth_result == E_SOURCE_AUTHENTICATION_ACCEPTED)
+               return TRUE;
+
+       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED,
+               _("Cannot connect to the server, repeat the action once you login to the server."));
+
+       source = e_m365_connection_get_source (m365_cnc);
+
+       switch (auth_result) {
+       case E_SOURCE_AUTHENTICATION_ERROR:
+               cred_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+               break;
+       case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+               cred_reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+               break;
+       case E_SOURCE_AUTHENTICATION_REJECTED:
+               cred_reason = E_SOURCE_CREDENTIALS_REASON_REJECTED;
+               break;
+       default:
+               cred_reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+               break;
+       }
+
+       e_source_invoke_credentials_required (source, cred_reason, certificate_pem, certificate_errors, 
local_error, NULL, NULL, NULL);
+
+       g_clear_error (&local_error);
+
+       return FALSE;
+}
+
+static ESource *
+ecb_ews_m365_find_helper_source (ESourceRegistry *registry,
+                                ESource *calendar_source)
+{
+       ESource *result = NULL;
+       ESource *collection_source;
+       GList *sources, *link;
+       const gchar *parent_uid;
+
+       collection_source = e_source_registry_find_extension (registry, calendar_source, 
E_SOURCE_EXTENSION_COLLECTION);
+       if (!collection_source)
+               return NULL;
+
+       parent_uid = e_source_get_uid (collection_source);
+       sources = e_source_registry_list_sources (registry, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+       for (link = sources; link; link = g_list_next (link)) {
+               ESource *adept = link->data;
+
+               if (g_strcmp0 (parent_uid, e_source_get_parent (adept)) == 0) {
+                       ESourceAuthentication *auth_extension;
+
+                       auth_extension = e_source_get_extension (adept, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+                       if (g_strcmp0 ("Microsoft365", e_source_authentication_get_method (auth_extension)) 
== 0) {
+                               result = g_object_ref (adept);
+                               break;
+                       }
+               }
+       }
+
+       g_list_free_full (sources, g_object_unref);
+
+       return result;
+}
+
+gboolean
+ecb_ews_save_as_online_meeting_sync (ESourceRegistry *registry,
+                                    EEwsConnection *ews_cnc,
+                                    ETimezoneCache *timezone_cache,
+                                    ECalComponent *comp,
+                                    gchar **out_new_uid,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       CamelEwsSettings *ews_settings;
+       CamelM365Settings *m365_settings;
+       EM365Connection *m365_cnc;
+       ESource *source;
+       gboolean success = FALSE;
+
+       ews_settings = e_ews_connection_ref_settings (ews_cnc);
+       m365_settings = g_object_new (CAMEL_TYPE_M365_SETTINGS, NULL);
+
+       camel_m365_settings_set_concurrent_connections (m365_settings, 1);
+
+       e_binding_bind_property (
+               ews_settings, "user",
+               m365_settings, "user",
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               ews_settings, "timeout",
+               m365_settings, "timeout",
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               ews_settings, "use-impersonation",
+               m365_settings, "use-impersonation",
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               ews_settings, "impersonate-user",
+               m365_settings, "impersonate-user",
+               G_BINDING_SYNC_CREATE);
+
+       /* Only the above properties are used in the m365_cnc */
+
+       source = ecb_ews_m365_find_helper_source (registry, e_ews_connection_get_source (ews_cnc));
+
+       if (!source) {
+               /* Translators: The '%s' is replaced with a UID of the collection source */
+               g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("Cannot find Microsoft365 helper 
source for calendar ā€œ%sā€"),
+                       e_source_get_uid (e_ews_connection_get_source (ews_cnc)));
+
+               g_clear_object (&ews_settings);
+               g_clear_object (&m365_settings);
+
+               return FALSE;
+       }
+
+       m365_cnc = e_m365_connection_new_full (source, m365_settings, FALSE);
+       g_clear_object (&source);
+
+       e_binding_bind_property (
+               ews_cnc, "proxy-resolver",
+               m365_cnc, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
+
+       success = ecb_ews_m365_authenticate (m365_cnc, cancellable, error);
+
+       if (success) {
+               ICalComponent *new_comp;
+               JsonBuilder *builder;
+
+               new_comp = e_cal_component_get_icalcomponent (comp);
+
+               e_m365_tz_utils_ref_windows_zones ();
+
+               builder = e_cal_backend_m365_utils_ical_to_json (m365_cnc, NULL, NULL, timezone_cache, 
I_CAL_VEVENT_COMPONENT,
+                       new_comp, NULL, cancellable, error);
+
+               if (builder) {
+                       JsonObject *created_item = NULL;
+
+                       success = e_m365_connection_create_event_sync (m365_cnc, NULL, NULL, NULL, builder, 
&created_item, cancellable, error);
+
+                       if (success && created_item) {
+                               const gchar *m365_id = e_m365_event_get_id (created_item);
+
+                               success = e_cal_backend_m365_utils_ical_to_json_2nd_go (m365_cnc, NULL, NULL, 
timezone_cache,
+                                       I_CAL_VEVENT_COMPONENT, new_comp, NULL, m365_id, cancellable, error);
+                       }
+
+                       if (success && created_item)
+                               *out_new_uid = g_strdup (e_m365_event_get_id (created_item));
+
+                       g_clear_pointer (&created_item, json_object_unref);
+                       g_clear_object (&builder);
+               }
+
+               e_m365_tz_utils_unref_windows_zones ();
+       }
+
+       g_clear_object (&ews_settings);
+       g_clear_object (&m365_settings);
+       g_clear_object (&m365_cnc);
+
+       return success;
+}
diff --git a/src/EWS/calendar/e-cal-backend-ews-m365.h b/src/EWS/calendar/e-cal-backend-ews-m365.h
new file mode 100644
index 00000000..2345fa22
--- /dev/null
+++ b/src/EWS/calendar/e-cal-backend-ews-m365.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_CAL_BACKEND_EWS_M365_H
+#define E_CAL_BACKEND_EWS_M365_H
+
+#include <libecal/libecal.h>
+#include <libedataserver/libedataserver.h>
+
+#include "common/e-ews-connection.h"
+
+G_BEGIN_DECLS
+
+gboolean       ecb_ews_save_as_online_meeting_sync     (ESourceRegistry *registry,
+                                                        EEwsConnection *ews_cnc,
+                                                        ETimezoneCache *timezone_cache,
+                                                        ECalComponent *comp,
+                                                        gchar **out_new_uid,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+
+G_END_DECLS
+
+#endif /* E_CAL_BACKEND_EWS_M365_H */
diff --git a/src/EWS/calendar/e-cal-backend-ews.c b/src/EWS/calendar/e-cal-backend-ews.c
index de719c79..3ab00a1e 100644
--- a/src/EWS/calendar/e-cal-backend-ews.c
+++ b/src/EWS/calendar/e-cal-backend-ews.c
@@ -31,6 +31,7 @@
 
 #include "e-cal-backend-ews.h"
 #include "e-cal-backend-ews-utils.h"
+#include "e-cal-backend-ews-m365.h"
 
 #ifndef O_BINARY
 #define O_BINARY 0
@@ -66,6 +67,8 @@ struct _ECalBackendEwsPrivate {
        gboolean is_freebusy_calendar;
 
        gchar *attachments_dir;
+
+       EThreeState is_user_calendar;
 };
 
 #define ECB_EWS_SYNC_TAG_STAMP_KEY "ews-sync-tag-stamp"
@@ -3089,6 +3092,7 @@ ecb_ews_save_component_sync (ECalMetaBackend *meta_backend,
        EwsFolderId *fid;
        GSList *link;
        const gchar *uid;
+       gboolean is_online_meeting;
        gboolean success = TRUE;
 
        g_return_val_if_fail (E_IS_CAL_BACKEND_EWS (meta_backend), FALSE);
@@ -3115,6 +3119,7 @@ ecb_ews_save_component_sync (ECalMetaBackend *meta_backend,
 
        g_rec_mutex_lock (&cbews->priv->cnc_lock);
 
+       is_online_meeting = e_cal_util_component_has_x_property (e_cal_component_get_icalcomponent (master), 
"X-M365-ONLINE-MEETING");
        uid = e_cal_component_get_uid (master);
        fid = e_ews_folder_id_new (cbews->priv->folder_id, NULL, FALSE);
 
@@ -3190,6 +3195,47 @@ ecb_ews_save_component_sync (ECalMetaBackend *meta_backend,
                   !ecb_ews_organizer_is_user (cbews, master)) {
                success = FALSE;
                g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_PERMISSION_DENIED, _("Cannot create 
meetings organized by other users in an Exchange Web Services calendar.")));
+       } else if (is_online_meeting && !instances->next) {
+               /* Check whether the folder is the user calendar */
+               if (cbews->priv->is_user_calendar == E_THREE_STATE_INCONSISTENT) {
+                       GSList folder_ids, *folders = NULL;
+                       EwsFolderId dfid;
+
+                       memset (&dfid, 0, sizeof (dfid));
+                       memset (&folder_ids, 0, sizeof (folder_ids));
+
+                       dfid.id = (gchar *) "calendar";
+                       dfid.is_distinguished_id = TRUE;
+
+                       folder_ids.data = &dfid;
+
+                       success = e_ews_connection_get_folder_sync (cbews->priv->cnc, G_PRIORITY_DEFAULT, 
"IdOnly", NULL, &folder_ids, &folders, cancellable, error);
+                       if (success) {
+                               gboolean is_user_calendar = FALSE;
+
+                               if (folders) {
+                                       EEwsFolder *folder = folders->data;
+                                       const EwsFolderId *fid = folder ? e_ews_folder_get_id (folder) : NULL;
+
+                                       is_user_calendar = fid && g_strcmp0 (cbews->priv->folder_id, fid->id) 
== 0;
+                               }
+
+                               g_slist_free_full (folders, g_object_unref);
+
+                               cbews->priv->is_user_calendar = is_user_calendar ? E_THREE_STATE_ON : 
E_THREE_STATE_OFF;
+                       }
+               }
+
+               if (success) {
+                       /* Verify the folder is the user calendar, the online meeting cannot be created 
elsewhere */
+                       success = cbews->priv->is_user_calendar == E_THREE_STATE_ON;
+
+                       if (!success)
+                               g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_PERMISSION_DENIED, 
_("Online meeting can be created only in the main user Calendar.")));
+               }
+
+               success = success && ecb_ews_save_as_online_meeting_sync (e_cal_backend_get_registry 
(E_CAL_BACKEND (cbews)),
+                       cbews->priv->cnc, E_TIMEZONE_CACHE (cbews), master, out_new_uid, cancellable, error);
        } else {
                GHashTable *removed_indexes;
                EwsCalendarConvertData convert_data = { 0 };
@@ -4653,6 +4699,7 @@ static void
 e_cal_backend_ews_init (ECalBackendEws *cbews)
 {
        cbews->priv = e_cal_backend_ews_get_instance_private (cbews);
+       cbews->priv->is_user_calendar = E_THREE_STATE_INCONSISTENT;
 
        g_rec_mutex_init (&cbews->priv->cnc_lock);
 
diff --git a/src/EWS/evolution/CMakeLists.txt b/src/EWS/evolution/CMakeLists.txt
index 70f65bde..8c11d2cf 100644
--- a/src/EWS/evolution/CMakeLists.txt
+++ b/src/EWS/evolution/CMakeLists.txt
@@ -35,6 +35,8 @@ set(sources
        e-mail-parser-ews-sharing-metadata.h
        e-mail-part-ews-sharing-metadata.c
        e-mail-part-ews-sharing-metadata.h
+       e-ews-comp-editor-extension.c
+       e-ews-comp-editor-extension.h
        e-ews-config-lookup.c
        e-ews-config-lookup.h
        e-ews-config-ui-extension.c
@@ -56,18 +58,21 @@ set(extra_defines)
 set(extra_cflags
        ${EVOLUTION_MAIL_CFLAGS}
        ${EVOLUTION_SHELL_CFLAGS}
+       ${EVOLUTION_CALENDAR_CFLAGS}
        ${LIBECAL_CFLAGS}
        ${LIBEBOOK_CFLAGS}
 )
 set(extra_incdirs
        ${EVOLUTION_MAIL_INCLUDE_DIRS}
        ${EVOLUTION_SHELL_INCLUDE_DIRS}
+       ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
        ${LIBECAL_INCLUDE_DIRS}
        ${LIBEBOOK_INCLUDE_DIRS}
 )
 set(extra_ldflags
        ${EVOLUTION_MAIL_LDFLAGS}
        ${EVOLUTION_SHELL_LDFLAGS}
+       ${EVOLUTION_CALENDAR_LDFLAGS}
        ${LIBECAL_LDFLAGS}
        ${LIBEBOOK_LDFLAGS}
 )
diff --git a/src/EWS/evolution/e-ews-comp-editor-extension.c b/src/EWS/evolution/e-ews-comp-editor-extension.c
new file mode 100644
index 00000000..ff7a900c
--- /dev/null
+++ b/src/EWS/evolution/e-ews-comp-editor-extension.c
@@ -0,0 +1,273 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+#include <glib-object.h>
+
+#include <calendar/gui/e-comp-editor.h>
+#include <calendar/gui/e-comp-editor-event.h>
+#include <calendar/gui/e-comp-editor-page-general.h>
+
+#include "e-ews-comp-editor-extension.h"
+
+#define E_TYPE_EWS_COMP_EDITOR_EXTENSION (e_ews_comp_editor_extension_get_type ())
+
+GType e_ews_comp_editor_extension_get_type (void);
+
+typedef struct _EEwsCompEditorExtension {
+       EExtension parent;
+} EEwsCompEditorExtension;
+
+typedef struct _EEwsCompEditorExtensionClass {
+       EExtensionClass parent_class;
+} EEwsCompEditorExtensionClass;
+
+G_DEFINE_DYNAMIC_TYPE (EEwsCompEditorExtension, e_ews_comp_editor_extension, E_TYPE_EXTENSION)
+
+static void
+e_ews_comp_editor_extension_update_actions (ECompEditor *comp_editor)
+{
+       GtkAction *action;
+       gboolean can_use = FALSE;
+
+       action = e_comp_editor_get_action (comp_editor, "ews-online-meeting");
+
+       g_return_if_fail (action != NULL);
+
+       /* Requirements to see the action:
+        *  - it's a new component (the option cannot be changed, it can be only set on create)
+        *  - it is a meeting
+        *  - the target calendar is EWS
+        *  - the authentication method is OAuth2
+        *
+        * It would check the host this connects to, but there are different
+        * end points, not only outlook.office365.com, thus check whether the auth
+        * method is OAuth2. It is also not accurate, because on-premise Exchange
+        * servers can have configured OAuth2, without using the Microsoft servers,
+        * but it might be good enough test.
+        */
+       can_use = (e_comp_editor_get_flags (comp_editor) & E_COMP_EDITOR_FLAG_IS_NEW) != 0;
+
+       if (can_use) {
+               ECompEditorPage *page_general;
+
+               page_general = e_comp_editor_get_page (comp_editor, E_TYPE_COMP_EDITOR_PAGE_GENERAL);
+
+               can_use = page_general && e_comp_editor_page_general_get_show_attendees 
(E_COMP_EDITOR_PAGE_GENERAL (page_general));
+       }
+
+       if (can_use) {
+               ECalClient *target_client;
+
+               target_client = e_comp_editor_get_target_client (comp_editor);
+
+               can_use = target_client != NULL;
+
+               if (can_use) {
+                       ESource *source;
+
+                       source = e_client_get_source (E_CLIENT (target_client));
+
+                       can_use = source && e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR) &&
+                               g_strcmp0 (e_source_backend_get_backend_name (e_source_get_extension (source, 
E_SOURCE_EXTENSION_CALENDAR)), "ews") == 0;
+
+                       if (can_use) {
+                               ESourceRegistry *registry;
+                               ESource *collection_source;
+
+                               registry = e_shell_get_registry (e_comp_editor_get_shell (comp_editor));
+                               collection_source = e_source_registry_find_extension (registry, source, 
E_SOURCE_EXTENSION_COLLECTION);
+                               can_use = collection_source && e_source_has_extension (source, 
E_SOURCE_EXTENSION_AUTHENTICATION);
+
+                               if (can_use) {
+                                       EOAuth2Services *oauth2_services;
+                                       const gchar *method;
+
+                                       oauth2_services = e_source_registry_get_oauth2_services (registry);
+                                       method = e_source_authentication_get_method (e_source_get_extension 
(source, E_SOURCE_EXTENSION_AUTHENTICATION));
+
+                                       can_use = method && e_oauth2_services_is_oauth2_alias 
(oauth2_services, method);
+                               }
+
+                               g_clear_object (&collection_source);
+                       }
+               }
+       }
+
+       gtk_action_set_visible (action, can_use);
+}
+
+static void
+e_ews_comp_editor_extension_fill_widgets_cb (ECompEditor *comp_editor,
+                                            ICalComponent *component)
+{
+       GtkAction *action;
+
+       action = e_comp_editor_get_action (comp_editor, "ews-online-meeting");
+
+       if (action)
+               gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), FALSE);
+
+       e_ews_comp_editor_extension_update_actions (comp_editor);
+}
+
+static gboolean
+e_ews_comp_editor_extension_fill_component_cb (ECompEditor *comp_editor,
+                                              ICalComponent *component)
+{
+       GtkAction *action;
+
+       action = e_comp_editor_get_action (comp_editor, "ews-online-meeting");
+
+       if (action && gtk_action_get_visible (action) &&
+           gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) {
+               e_cal_util_component_set_x_property (component, "X-M365-ONLINE-MEETING", "1");
+       } else {
+               e_cal_util_component_remove_x_property (component, "X-M365-ONLINE-MEETING");
+       }
+
+       return TRUE;
+}
+
+static void
+e_ews_comp_editor_extension_handle_map_unmap (ECompEditor *comp_editor,
+                                             gboolean is_map)
+{
+       ECompEditorPage *page_general;
+
+       /* Cannot do this in the 'constructed' method, because the extensions are loaded
+          before the Event editor fills the comp editor with the pages. */
+       page_general = e_comp_editor_get_page (comp_editor, E_TYPE_COMP_EDITOR_PAGE_GENERAL);
+
+       if (page_general) {
+               if (is_map) {
+                       g_signal_connect_object (page_general, "notify::show-attendees",
+                               G_CALLBACK (e_ews_comp_editor_extension_update_actions), comp_editor,
+                               G_CONNECT_SWAPPED);
+               } else {
+                       g_signal_handlers_disconnect_by_func (page_general,
+                               G_CALLBACK (e_ews_comp_editor_extension_update_actions), comp_editor);
+               }
+       }
+}
+
+static void
+e_ews_comp_editor_extension_map_cb (ECompEditor *comp_editor,
+                                   gpointer user_data)
+{
+       e_ews_comp_editor_extension_handle_map_unmap (comp_editor, TRUE);
+}
+
+static void
+e_ews_comp_editor_extension_unmap_cb (ECompEditor *comp_editor,
+                                     gpointer user_data)
+{
+       e_ews_comp_editor_extension_handle_map_unmap (comp_editor, FALSE);
+}
+
+static void
+e_ews_comp_editor_extension_constructed (GObject *object)
+{
+       const gchar *ui =
+               "<ui>"
+               "  <menubar action='main-menu'>"
+               "    <menu action='options-menu'>"
+               "      <placeholder name='toggles'>"
+               "        <menuitem action='ews-online-meeting'/>"
+               "      </placeholder>"
+               "    </menu>"
+               "  </menubar>"
+               "  <toolbar name='main-toolbar'>"
+               "    <placeholder name='content'>\n"
+               "      <toolitem action='ews-online-meeting'/>\n"
+               "    </placeholder>"
+               "  </toolbar>"
+               "</ui>";
+
+       GtkToggleActionEntry entries[] = {
+               { "ews-online-meeting",
+                 "stock_people",
+                 N_("Online Meeting"),
+                 NULL,
+                 N_("Create the meeting as an online meeting in the main user calendar"),
+                 NULL,
+                 FALSE }
+       };
+       EExtensible *extensible;
+
+       /* Chain up to parent's method */
+       G_OBJECT_CLASS (e_ews_comp_editor_extension_parent_class)->constructed (object);
+
+       extensible = e_extension_get_extensible (E_EXTENSION (object));
+
+       if (E_IS_COMP_EDITOR_EVENT (extensible)) {
+               ECompEditor *comp_editor = E_COMP_EDITOR (extensible);
+               GtkUIManager *ui_manager;
+               GtkActionGroup *action_group;
+               GError *error = NULL;
+
+               ui_manager = e_comp_editor_get_ui_manager (comp_editor);
+               action_group = e_comp_editor_get_action_group (comp_editor, "individual");
+
+               gtk_action_group_add_toggle_actions (action_group, entries, G_N_ELEMENTS (entries), 
comp_editor);
+
+               gtk_ui_manager_add_ui_from_string (ui_manager, ui, -1, &error);
+
+               if (error) {
+                       g_critical ("%s: %s", G_STRFUNC, error->message);
+                       g_error_free (error);
+               }
+
+               g_signal_connect (comp_editor, "map",
+                       G_CALLBACK (e_ews_comp_editor_extension_map_cb), NULL);
+
+               g_signal_connect (comp_editor, "unmap",
+                       G_CALLBACK (e_ews_comp_editor_extension_unmap_cb), NULL);
+
+               g_signal_connect (comp_editor, "notify::target-client",
+                       G_CALLBACK (e_ews_comp_editor_extension_update_actions), NULL);
+
+               g_signal_connect (comp_editor, "notify::flags",
+                       G_CALLBACK (e_ews_comp_editor_extension_update_actions), NULL);
+
+               g_signal_connect (comp_editor, "fill-widgets",
+                       G_CALLBACK (e_ews_comp_editor_extension_fill_widgets_cb), NULL);
+
+               g_signal_connect (comp_editor, "fill-component",
+                       G_CALLBACK (e_ews_comp_editor_extension_fill_component_cb), NULL);
+       }
+}
+
+static void
+e_ews_comp_editor_extension_class_init (EEwsCompEditorExtensionClass *klass)
+{
+       GObjectClass *object_class;
+       EExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_ews_comp_editor_extension_constructed;
+
+       extension_class = E_EXTENSION_CLASS (klass);
+       extension_class->extensible_type = E_TYPE_COMP_EDITOR;
+}
+
+static void
+e_ews_comp_editor_extension_class_finalize (EEwsCompEditorExtensionClass *klass)
+{
+}
+
+static void
+e_ews_comp_editor_extension_init (EEwsCompEditorExtension *extension)
+{
+}
+
+void
+e_ews_comp_editor_extension_type_register (GTypeModule *type_module)
+{
+       e_ews_comp_editor_extension_register_type (type_module);
+}
diff --git a/src/EWS/evolution/e-ews-comp-editor-extension.h b/src/EWS/evolution/e-ews-comp-editor-extension.h
new file mode 100644
index 00000000..89360463
--- /dev/null
+++ b/src/EWS/evolution/e-ews-comp-editor-extension.h
@@ -0,0 +1,18 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat, Inc. (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_EWS_COMP_EDITOR_EXTENSION_H
+#define E_EWS_COMP_EDITOR_EXTENSION_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+void   e_ews_comp_editor_extension_type_register       (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_EWS_COMP_EDITOR_EXTENSION_H */
diff --git a/src/EWS/evolution/module-ews-configuration.c b/src/EWS/evolution/module-ews-configuration.c
index 747fc8db..7ce0e8fe 100644
--- a/src/EWS/evolution/module-ews-configuration.c
+++ b/src/EWS/evolution/module-ews-configuration.c
@@ -27,6 +27,7 @@
 #include "e-ews-config-lookup.h"
 #include "e-ews-photo-source.h"
 
+#include "e-ews-comp-editor-extension.h"
 #include "e-ews-config-ui-extension.h"
 #include "common/camel-sasl-xoauth2-office365.h"
 #include "common/e-oauth2-service-office365.h"
@@ -57,6 +58,7 @@ e_module_load (GTypeModule *type_module)
        e_mail_parser_ews_multipart_mixed_type_register (type_module);
        e_mail_parser_ews_sharing_metadata_type_register (type_module);
        e_mail_part_ews_sharing_metadata_type_register (type_module);
+       e_ews_comp_editor_extension_type_register (type_module);
        e_ews_config_lookup_type_register (type_module);
        e_ews_config_ui_extension_type_register (type_module);
        e_ews_ooo_notificator_type_register (type_module);
diff --git a/src/EWS/registry/CMakeLists.txt b/src/EWS/registry/CMakeLists.txt
index b8aaa16f..2c131cfb 100644
--- a/src/EWS/registry/CMakeLists.txt
+++ b/src/EWS/registry/CMakeLists.txt
@@ -5,6 +5,8 @@ set(sources
        e-ews-backend.h
        e-ews-backend-factory.c
        e-ews-backend-factory.h
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/camel-m365-settings.c
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365/common/camel-m365-settings.h
 )
 set(extra_defines)
 set(extra_cflags)
diff --git a/src/EWS/registry/e-ews-backend.c b/src/EWS/registry/e-ews-backend.c
index 8b03b37e..80289ae2 100644
--- a/src/EWS/registry/e-ews-backend.c
+++ b/src/EWS/registry/e-ews-backend.c
@@ -12,6 +12,9 @@
 
 #include "common/e-ews-connection-utils.h"
 #include "common/e-source-ews-folder.h"
+#include "../Microsoft365/common/camel-m365-settings.h"
+
+#define EWS_HELPER_M365_RESOURCE_ID "helper-m365-calendar"
 
 typedef struct _SyncFoldersClosure SyncFoldersClosure;
 
@@ -591,6 +594,77 @@ ews_backend_add_gal_source (EEwsBackend *backend)
        g_object_unref (source);
 }
 
+/* This source is used by the Calendar backend, when creating online meeting */
+static void
+ews_backend_maybe_add_m365_source (EEwsBackend *ews_backend)
+{
+       CamelEwsSettings *ews_settings;
+       ECollectionBackend *collection_backend;
+       ESource *collection_source;
+       ESource *m365_source;
+       ESourceAuthentication *auth_extension;
+       ESourceAuthentication *collection_auth_extension;
+       ESourceCollection *collection_extension = NULL;
+       ESourceExtension *source_extension;
+       ESourceRegistryServer *server;
+       gboolean can_enable;
+       gchar *display_name;
+
+       ews_settings = ews_backend_get_settings (ews_backend);
+
+       if (camel_ews_settings_get_auth_mechanism (ews_settings) != EWS_AUTH_TYPE_OAUTH2)
+               return;
+
+       collection_source = e_backend_get_source (E_BACKEND (ews_backend));
+
+       if (!collection_source)
+               return;
+
+       /* Make sure the ESourceCamel knows about it, even when no Microsoft365 mail account is created */
+       e_source_camel_generate_subtype ("microsoft365", CAMEL_TYPE_M365_SETTINGS);
+
+       if (e_source_has_extension (collection_source, E_SOURCE_EXTENSION_COLLECTION))
+               collection_extension = e_source_get_extension (collection_source, 
E_SOURCE_EXTENSION_COLLECTION);
+
+       can_enable = !collection_extension || (e_source_get_enabled (collection_source) &&
+               e_source_collection_get_contacts_enabled (collection_extension));
+
+       collection_backend = E_COLLECTION_BACKEND (ews_backend);
+       m365_source = e_collection_backend_new_child (collection_backend, EWS_HELPER_M365_RESOURCE_ID);
+       e_source_set_enabled (m365_source, can_enable);
+
+       display_name = g_strconcat (e_source_get_display_name (collection_source), " (Microsoft365)", NULL);
+
+       source_extension = e_source_get_extension (m365_source, e_source_camel_get_extension_name 
("microsoft365"));
+       if (source_extension) {
+               CamelSettings *settings = e_source_camel_get_settings (E_SOURCE_CAMEL (source_extension));
+
+               if (settings) {
+                       g_object_set (settings,
+                               "host", "graph.microsoft.com",
+                               "auth-mechanism", "Microsoft365",
+                               NULL);
+               }
+       }
+
+       collection_auth_extension = e_source_get_extension (collection_source, 
E_SOURCE_EXTENSION_AUTHENTICATION);
+
+       e_source_set_display_name (m365_source, display_name);
+
+       auth_extension = e_source_get_extension (m365_source, E_SOURCE_EXTENSION_AUTHENTICATION);
+       e_source_authentication_set_host (auth_extension, "graph.microsoft.com");
+       e_source_authentication_set_method (auth_extension, "Microsoft365");
+       e_source_authentication_set_user (auth_extension,
+               e_source_authentication_get_user (collection_auth_extension));
+
+       server = e_collection_backend_ref_server (collection_backend);
+       e_source_registry_server_add_source (server, m365_source);
+
+       g_object_unref (m365_source);
+       g_object_unref (server);
+       g_free (display_name);
+}
+
 static void ews_backend_populate (ECollectionBackend *backend);
 
 static void
@@ -838,6 +912,7 @@ ews_backend_populate (ECollectionBackend *collection_backend)
        }
 
        ews_backend_add_gal_source (ews_backend);
+       ews_backend_maybe_add_m365_source (ews_backend);
        ews_backend_claim_old_resources (collection_backend);
 
        if (e_backend_get_online (backend)) {
@@ -864,6 +939,15 @@ ews_backend_dup_resource_id (ECollectionBackend *backend,
        ESourceEwsFolder *extension;
        const gchar *extension_name;
 
+       if (e_source_has_extension (child_source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (child_source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               if (g_strcmp0 (e_source_authentication_get_method (auth_extension), "Microsoft365") == 0)
+                       return g_strdup (EWS_HELPER_M365_RESOURCE_ID);
+       }
+
        extension_name = E_SOURCE_EXTENSION_EWS_FOLDER;
        extension = e_source_get_extension (child_source, extension_name);
 
@@ -882,25 +966,29 @@ ews_backend_child_added (ECollectionBackend *backend,
        extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
        if (e_source_has_extension (child_source, extension_name)) {
                ESourceAuthentication *auth_child_extension;
-               ESourceAuthentication *auth_collection_extension;
 
                auth_child_extension = e_source_get_extension (child_source, extension_name);
-               auth_collection_extension = e_source_get_extension (collection_source, extension_name);
-
-               e_binding_bind_property (
-                       auth_collection_extension, "host",
-                       auth_child_extension, "host",
-                       G_BINDING_SYNC_CREATE);
-
-               e_binding_bind_property (
-                       auth_collection_extension, "user",
-                       auth_child_extension, "user",
-                       G_BINDING_SYNC_CREATE);
-
-               e_binding_bind_property (
-                       auth_collection_extension, "method",
-                       auth_child_extension, "method",
-                       G_BINDING_SYNC_CREATE);
+
+               if (g_strcmp0 (e_source_authentication_get_method (auth_child_extension), "Microsoft365") != 
0) {
+                       ESourceAuthentication *auth_collection_extension;
+
+                       auth_collection_extension = e_source_get_extension (collection_source, 
extension_name);
+
+                       e_binding_bind_property (
+                               auth_collection_extension, "host",
+                               auth_child_extension, "host",
+                               G_BINDING_SYNC_CREATE);
+
+                       e_binding_bind_property (
+                               auth_collection_extension, "user",
+                               auth_child_extension, "user",
+                               G_BINDING_SYNC_CREATE);
+
+                       e_binding_bind_property (
+                               auth_collection_extension, "method",
+                               auth_child_extension, "method",
+                               G_BINDING_SYNC_CREATE);
+               }
        }
 
        /* We track EWS folders in a hash table by folder ID. */
diff --git a/src/Microsoft365/common/e-m365-connection.c b/src/Microsoft365/common/e-m365-connection.c
index 02664874..2d62fbf8 100644
--- a/src/Microsoft365/common/e-m365-connection.c
+++ b/src/Microsoft365/common/e-m365-connection.c
@@ -295,9 +295,12 @@ m365_connection_constructed (GObject *object)
        soup_session_add_feature_by_type (cnc->priv->soup_session, E_TYPE_SOUP_AUTH_BEARER);
 
        /* This can use only OAuth2 authentication, which should be set on the ESource */
-       soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_BASIC);
-       soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_NTLM);
-       soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_NEGOTIATE);
+       if (soup_session_has_feature (cnc->priv->soup_session, SOUP_TYPE_AUTH_BASIC))
+               soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_BASIC);
+       if (soup_session_has_feature (cnc->priv->soup_session, SOUP_TYPE_AUTH_NTLM))
+               soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_NTLM);
+       if (soup_session_has_feature (cnc->priv->soup_session, SOUP_TYPE_AUTH_NEGOTIATE))
+               soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_NEGOTIATE);
        soup_session_add_feature_by_type (cnc->priv->soup_session, E_TYPE_SOUP_AUTH_BEARER);
 
        cnc->priv->hash_key = camel_network_settings_dup_user (CAMEL_NETWORK_SETTINGS (cnc->priv->settings));
@@ -791,11 +794,17 @@ e_m365_connection_json_node_from_message (SoupMessage *message,
 
                if (content_type && g_ascii_strcasecmp (content_type, "application/json") == 0) {
                        JsonParser *json_parser;
+                       GByteArray *msg_bytes = NULL;
 
                        json_parser = json_parser_new_immutable ();
 
+                       if (!input_stream)
+                               msg_bytes = e_soup_session_util_get_message_bytes (message);
+
                        if (input_stream) {
                                success = json_parser_load_from_stream (json_parser, input_stream, 
cancellable, error);
+                       } else if (msg_bytes) {
+                               success = json_parser_load_from_data (json_parser, (const gchar *) 
msg_bytes->data, msg_bytes->len, error);
                        } else {
                                /* This should not happen, it's for safety check only, thus the string is not 
localized */
                                success = FALSE;
@@ -1013,6 +1022,17 @@ m365_connection_send_request_sync (EM365Connection *cnc,
 
                                if (node)
                                        json_node_unref (node);
+                       } else if (e_soup_session_util_get_message_bytes (message)) {
+                               /* Try to extract detailed error */
+                               JsonNode *node = NULL;
+
+                               if (!e_m365_connection_json_node_from_message (message, input_stream, &node, 
cancellable, &local_error) && local_error) {
+                                       g_clear_error (error);
+                                       g_propagate_error (error, local_error);
+                                       local_error = NULL;
+                               }
+
+                               g_clear_pointer (&node, json_node_unref);
                        }
 
                        g_clear_object (&input_stream);
@@ -3865,7 +3885,6 @@ e_m365_connection_create_event_sync (EM365Connection *cnc,
        gchar *uri;
 
        g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
-       g_return_val_if_fail (calendar_id != NULL, FALSE);
        g_return_val_if_fail (event != NULL, FALSE);
        g_return_val_if_fail (out_created_event != NULL, FALSE);
 
@@ -3878,7 +3897,7 @@ e_m365_connection_create_event_sync (EM365Connection *cnc,
                        "", "events",
                        NULL);
        } else {
-               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, "me",
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, "users",
                        "events", NULL, NULL, NULL);
        }
 
@@ -4422,7 +4441,6 @@ e_m365_connection_add_event_attachment_sync (EM365Connection *cnc,
        gchar *uri;
 
        g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
-       g_return_val_if_fail (calendar_id != NULL, FALSE);
        g_return_val_if_fail (event_id != NULL, FALSE);
        g_return_val_if_fail (in_attachment != NULL, FALSE);
 
@@ -4437,7 +4455,7 @@ e_m365_connection_add_event_attachment_sync (EM365Connection *cnc,
                        "", "attachments",
                        NULL);
        } else {
-               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, "me",
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, "users",
                        "events", NULL, NULL,
                        "", event_id,
                        "", "attachments",
diff --git a/src/Microsoft365/common/e-oauth2-service-microsoft365.c 
b/src/Microsoft365/common/e-oauth2-service-microsoft365.c
index 3b51384c..3cf3fabe 100644
--- a/src/Microsoft365/common/e-oauth2-service-microsoft365.c
+++ b/src/Microsoft365/common/e-oauth2-service-microsoft365.c
@@ -430,6 +430,9 @@ e_oauth2_service_microsoft365_class_init (EOAuth2ServiceMicrosoft365Class *klass
 
        object_class = G_OBJECT_CLASS (klass);
        object_class->finalize = eos_microsoft365_finalize;
+
+       /* Make sure the ESourceCamel knows about it, even when no Microsoft365 mail account is created */
+       e_source_camel_generate_subtype ("microsoft365", CAMEL_TYPE_M365_SETTINGS);
 }
 
 static void


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]