/* * e-mail-autoconfig.c * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . * */ /* XXX Thoughts on RFC 6186: Use of SRV Records for Locating Email * Submission/Access Services * * RFC 6186 specifies using SRV DNS lookups to aid in automatic * configuration of mail accounts. While it may be tempting to * implement the RFC here (I was tempted at least), upon closer * examination I find the RFC to be insufficient. * * An SRV DNS lookup only provides a host name and port number. * The RFC assumes the account's user name can be derived from * the email address, and suggests probing the mail server for * a valid user name by actually attempting authentication, * first with the user's full email address and then falling * back to only the local part. * * I'm uncomfortable with this for a number of reasons: * * 1) I would prefer the user have a chance to manually review * the settings before transmitting credentials of any kind, * since DNS responses can be spoofed. * * 2) Authentication at this phase would require asking for * a password either before or during auto-configuration. * Asking before assumes a password-based authentication * mechanism is to be used, which is not always the case, * and asking during may raise the user's suspicion about * what's going on behind the scenes (it would mine). * * 3) For better or worse, our architecture isn't really built * to handle authentication at this stage. EMailSession is * wired into too many other areas to be reused here without * risking unwanted side-effects, therefore it would require * a custom CamelSession subclass with an authenticate_sync() * implementation similar to EMailSession. * * While the technical limitations of (3) could be overcome, my concerns * in (1) and (2) still stand. I think for the time being a better solution * is to have an administrator script on api.gnome.org that compares the host * and port settings in each clientConfig file to the _imap._tcp, _pop3._tcp, * and _submission._tcp SRV records for that service provider (if available) * to help ensure the static XML content remains accurate. It would also be * instructive to track how many service providers even implement RFC 6186. * * Recording my thoughts here for posterity. -- mbarnes */ #include "e-mail-autoconfig.h" #include #include #include /* For error codes. */ #include #define E_MAIL_AUTOCONFIG_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_MAIL_AUTOCONFIG, EMailAutoconfigPrivate)) #define AUTOCONFIG_BASE_URI \ "http://api.gnome.org/evolution/autoconfig/1.1/" #define ERROR_IS_NOT_FOUND(error) \ (g_error_matches ((error), SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) typedef struct _EMailAutoconfigResult EMailAutoconfigResult; typedef struct _ParserClosure ParserClosure; struct _EMailAutoconfigResult { gboolean set; gchar *user; gchar *host; guint16 port; gchar *auth_mechanism; CamelNetworkSecurityMethod security_method; }; struct _EMailAutoconfigPrivate { ESourceRegistry *registry; gchar *email_address; gchar *email_local_part; gchar *email_domain_part; EMailAutoconfigResult imap_result; EMailAutoconfigResult pop3_result; EMailAutoconfigResult smtp_result; }; struct _ParserClosure { EMailAutoconfig *autoconfig; EMailAutoconfigResult *result; }; enum { PROP_0, PROP_EMAIL_ADDRESS, PROP_REGISTRY }; /* Forward Declarations */ static void e_mail_autoconfig_initable_init (GInitableIface *interface); /* By default, the GAsyncInitable interface calls GInitable.init() * from a separate thread, so we only have to override GInitable. */ G_DEFINE_TYPE_WITH_CODE ( EMailAutoconfig, e_mail_autoconfig, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE ( G_TYPE_INITABLE, e_mail_autoconfig_initable_init) G_IMPLEMENT_INTERFACE ( G_TYPE_ASYNC_INITABLE, NULL)) static void mail_autoconfig_parse_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { ParserClosure *closure = user_data; EMailAutoconfigPrivate *priv; gboolean is_incoming_server; gboolean is_outgoing_server; priv = closure->autoconfig->priv; is_incoming_server = g_str_equal (element_name, "incomingServer"); is_outgoing_server = g_str_equal (element_name, "outgoingServer"); if (is_incoming_server || is_outgoing_server) { const gchar *type = NULL; g_markup_collect_attributes ( element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING, "type", &type, G_MARKUP_COLLECT_INVALID); if (g_strcmp0 (type, "imap") == 0) closure->result = &priv->imap_result; if (g_strcmp0 (type, "pop3") == 0) closure->result = &priv->pop3_result; if (g_strcmp0 (type, "smtp") == 0) closure->result = &priv->smtp_result; } } static void mail_autoconfig_parse_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { ParserClosure *closure = user_data; gboolean is_incoming_server; gboolean is_outgoing_server; is_incoming_server = g_str_equal (element_name, "incomingServer"); is_outgoing_server = g_str_equal (element_name, "outgoingServer"); if (is_incoming_server || is_outgoing_server) closure->result = NULL; } static void mail_autoconfig_parse_text (GMarkupParseContext *context, const gchar *text, gsize text_length, gpointer user_data, GError **error) { ParserClosure *closure = user_data; EMailAutoconfigPrivate *priv; const gchar *element_name; GString *string; priv = closure->autoconfig->priv; if (closure->result == NULL) return; /* Perform the following text substitutions: * * %EMAILADDRESS% : closure->email_address * %EMAILLOCALPART% : closure->email_local_part * %EMAILDOMAIN% : closure->email_domain_part */ if (strchr (text, '%') == NULL) string = g_string_new (text); else { const gchar *cp = text; string = g_string_sized_new (256); while (*cp != '\0') { const gchar *variable; const gchar *substitute; if (*cp != '%') { g_string_append_c (string, *cp++); continue; } variable = "%EMAILADDRESS%"; substitute = priv->email_address; if (strncmp (cp, variable, strlen (variable)) == 0) { g_string_append (string, substitute); cp += strlen (variable); continue; } variable = "%EMAILLOCALPART%"; substitute = priv->email_local_part; if (strncmp (cp, variable, strlen (variable)) == 0) { g_string_append (string, substitute); cp += strlen (variable); continue; } variable = "%EMAILDOMAIN%"; substitute = priv->email_domain_part; if (strncmp (cp, variable, strlen (variable)) == 0) { g_string_append (string, substitute); cp += strlen (variable); continue; } g_string_append_c (string, *cp++); } } element_name = g_markup_parse_context_get_element (context); if (g_str_equal (element_name, "hostname")) { closure->result->host = g_strdup (string->str); closure->result->set = TRUE; } else if (g_str_equal (element_name, "username")) { closure->result->user = g_strdup (string->str); closure->result->set = TRUE; } else if (g_str_equal (element_name, "port")) { glong port = strtol (string->str, NULL, 10); if (port == CLAMP (port, 1, G_MAXUINT16)) { closure->result->port = (guint16) port; closure->result->set = TRUE; } } else if (g_str_equal (element_name, "socketType")) { if (g_str_equal (string->str, "plain")) { closure->result->security_method = CAMEL_NETWORK_SECURITY_METHOD_NONE; closure->result->set = TRUE; } else if (g_str_equal (string->str, "SSL")) { closure->result->security_method = CAMEL_NETWORK_SECURITY_METHOD_SSL_ON_ALTERNATE_PORT; closure->result->set = TRUE; } else if (g_str_equal (string->str, "STARTTLS")) { closure->result->security_method = CAMEL_NETWORK_SECURITY_METHOD_STARTTLS_ON_STANDARD_PORT; closure->result->set = TRUE; } } else if (g_str_equal (element_name, "authentication")) { gboolean use_plain_auth = FALSE; /* "password-cleartext" and "plain" are synonymous. */ if (g_str_equal (string->str, "password-cleartext")) use_plain_auth = TRUE; if (g_str_equal (string->str, "plain")) use_plain_auth = TRUE; if (use_plain_auth) { gchar *auth_mechanism = NULL; /* The exact auth name depends on the protocol. */ /* Leave this NULL for IMAP so Camel * will issue an IMAP LOGIN command. */ if (closure->result == &priv->imap_result) auth_mechanism = NULL; /* Leave this NULL for POP3 so Camel * will issue POP3 USER/PASS commands. */ if (closure->result == &priv->pop3_result) auth_mechanism = NULL; if (closure->result == &priv->smtp_result) auth_mechanism = g_strdup ("LOGIN"); closure->result->auth_mechanism = auth_mechanism; closure->result->set = TRUE; } /* XXX Other values not handled, * but they are corner cases for the most part. */ } g_string_free (string, TRUE); } static GMarkupParser mail_autoconfig_parser = { mail_autoconfig_parse_start_element, mail_autoconfig_parse_end_element, mail_autoconfig_parse_text }; static gchar * mail_autoconfig_resolve_name_server (const gchar *domain, GCancellable *cancellable, GError **error) { GResolver *resolver; GList *records; gchar *name_server = NULL; resolver = g_resolver_get_default (); records = g_resolver_lookup_records ( resolver, domain, G_RESOLVER_RECORD_NS, cancellable, error); /* This list is sorted per RFC 2782, so use the first item. */ if (records != NULL) { GVariant *variant = records->data; g_variant_get_child (variant, 0, "s", &name_server); } g_list_free_full (records, (GDestroyNotify) g_variant_unref); g_object_unref (resolver); return name_server; } static void mail_autoconfig_abort_soup_session_cb (GCancellable *cancellable, SoupSession *soup_session) { soup_session_abort (soup_session); } static gboolean mail_autoconfig_lookup (EMailAutoconfig *autoconfig, const gchar *domain, GCancellable *cancellable, GError **error) { GMarkupParseContext *context; ESourceRegistry *registry; ESource *proxy_source; SoupMessage *soup_message; SoupSession *soup_session; ParserClosure closure; gulong cancel_id = 0; gboolean success; guint status; gchar *uri; registry = e_mail_autoconfig_get_registry (autoconfig); proxy_source = e_source_registry_ref_builtin_proxy (registry); soup_session = soup_session_new_with_options ( SOUP_SESSION_PROXY_RESOLVER, G_PROXY_RESOLVER (proxy_source), NULL); g_object_unref (proxy_source); uri = g_strconcat (AUTOCONFIG_BASE_URI, domain, NULL); soup_message = soup_message_new (SOUP_METHOD_GET, uri); g_free (uri); if (G_IS_CANCELLABLE (cancellable)) cancel_id = g_cancellable_connect ( cancellable, G_CALLBACK (mail_autoconfig_abort_soup_session_cb), g_object_ref (soup_session), (GDestroyNotify) g_object_unref); status = soup_session_send_message (soup_session, soup_message); if (cancel_id > 0) g_cancellable_disconnect (cancellable, cancel_id); success = SOUP_STATUS_IS_SUCCESSFUL (status); if (!success) { g_set_error_literal ( error, SOUP_HTTP_ERROR, soup_message->status_code, soup_message->reason_phrase); goto exit; } closure.autoconfig = autoconfig; closure.result = NULL; context = g_markup_parse_context_new ( &mail_autoconfig_parser, 0, &closure, (GDestroyNotify) NULL); success = g_markup_parse_context_parse ( context, soup_message->response_body->data, soup_message->response_body->length, error); if (success) success = g_markup_parse_context_end_parse (context, error); g_markup_parse_context_free (context); exit: g_object_unref (soup_message); g_object_unref (soup_session); return success; } static gboolean mail_autoconfig_set_details (EMailAutoconfig *autoconfig, EMailAutoconfigResult *result, ESource *source, const gchar *extension_name) { ESourceCamel *camel_ext; ESourceBackend *backend_ext; CamelSettings *settings; const gchar *backend_name; g_return_val_if_fail (result != NULL, FALSE); if (!result->set) return FALSE; if (!e_source_has_extension (source, extension_name)) return FALSE; backend_ext = e_source_get_extension (source, extension_name); backend_name = e_source_backend_get_backend_name (backend_ext); extension_name = e_source_camel_get_extension_name (backend_name); camel_ext = e_source_get_extension (source, extension_name); settings = e_source_camel_get_settings (camel_ext); g_return_val_if_fail (CAMEL_IS_NETWORK_SETTINGS (settings), FALSE); g_object_set ( settings, "user", result->user, "host", result->host, "port", result->port, "auth-mechanism", result->auth_mechanism, "security-method", result->security_method, NULL); return TRUE; } static void mail_autoconfig_set_email_address (EMailAutoconfig *autoconfig, const gchar *email_address) { g_return_if_fail (email_address != NULL); g_return_if_fail (autoconfig->priv->email_address == NULL); autoconfig->priv->email_address = g_strdup (email_address); } static void mail_autoconfig_set_registry (EMailAutoconfig *autoconfig, ESourceRegistry *registry) { g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); g_return_if_fail (autoconfig->priv->registry == NULL); autoconfig->priv->registry = g_object_ref (registry); } static void mail_autoconfig_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_EMAIL_ADDRESS: mail_autoconfig_set_email_address ( E_MAIL_AUTOCONFIG (object), g_value_get_string (value)); return; case PROP_REGISTRY: mail_autoconfig_set_registry ( E_MAIL_AUTOCONFIG (object), g_value_get_object (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void mail_autoconfig_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_EMAIL_ADDRESS: g_value_set_string ( value, e_mail_autoconfig_get_email_address ( E_MAIL_AUTOCONFIG (object))); return; case PROP_REGISTRY: g_value_set_object ( value, e_mail_autoconfig_get_registry ( E_MAIL_AUTOCONFIG (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void mail_autoconfig_dispose (GObject *object) { EMailAutoconfigPrivate *priv; priv = E_MAIL_AUTOCONFIG_GET_PRIVATE (object); g_clear_object (&priv->registry); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_mail_autoconfig_parent_class)->dispose (object); } static void mail_autoconfig_finalize (GObject *object) { EMailAutoconfigPrivate *priv; priv = E_MAIL_AUTOCONFIG_GET_PRIVATE (object); g_free (priv->email_address); g_free (priv->email_local_part); g_free (priv->email_domain_part); g_free (priv->imap_result.user); g_free (priv->imap_result.host); g_free (priv->imap_result.auth_mechanism); g_free (priv->pop3_result.user); g_free (priv->pop3_result.host); g_free (priv->pop3_result.auth_mechanism); g_free (priv->smtp_result.user); g_free (priv->smtp_result.host); g_free (priv->smtp_result.auth_mechanism); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (e_mail_autoconfig_parent_class)->finalize (object); } static gboolean mail_autoconfig_initable_init (GInitable *initable, GCancellable *cancellable, GError **error) { EMailAutoconfig *autoconfig; const gchar *email_address; const gchar *domain; const gchar *cp; gchar *name_server; gboolean success = FALSE; GError *local_error = NULL; autoconfig = E_MAIL_AUTOCONFIG (initable); email_address = e_mail_autoconfig_get_email_address (autoconfig); if (email_address == NULL) { g_set_error_literal ( error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("No email address provided")); return FALSE; } cp = strchr (email_address, '@'); if (cp == NULL) { g_set_error_literal ( error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Missing domain in email address")); return FALSE; } domain = cp + 1; autoconfig->priv->email_local_part = g_strndup (email_address, cp - email_address); autoconfig->priv->email_domain_part = g_strdup (domain); /* First try the email address domain verbatim. */ success = mail_autoconfig_lookup ( autoconfig, domain, cancellable, &local_error); g_warn_if_fail ( (success && local_error == NULL) || (!success && local_error != NULL)); if (success) return TRUE; /* "404 Not Found" errors are non-fatal this time around. */ if (ERROR_IS_NOT_FOUND (local_error)) { g_clear_error (&local_error); } else { g_propagate_error (error, local_error); return FALSE; } /* Look up an authoritative name server for the email address * domain according to its "name server" (NS) DNS record. */ name_server = mail_autoconfig_resolve_name_server ( domain, cancellable, error); if (name_server == NULL) return FALSE; /* Widdle away segments of the name server domain until * we find a match, or until we widdle down to nothing. */ cp = name_server; while (cp != NULL && strchr (cp, '.') != NULL) { g_clear_error (&local_error); success = mail_autoconfig_lookup ( autoconfig, cp, cancellable, &local_error); g_warn_if_fail ( (success && local_error == NULL) || (!success && local_error != NULL)); if (success || !ERROR_IS_NOT_FOUND (local_error)) break; cp = strchr (cp, '.'); if (cp != NULL) cp++; } if (local_error != NULL) g_propagate_error (error, local_error); g_free (name_server); return success; } static void e_mail_autoconfig_class_init (EMailAutoconfigClass *class) { GObjectClass *object_class; g_type_class_add_private (class, sizeof (EMailAutoconfigPrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = mail_autoconfig_set_property; object_class->get_property = mail_autoconfig_get_property; object_class->dispose = mail_autoconfig_dispose; object_class->finalize = mail_autoconfig_finalize; g_object_class_install_property ( object_class, PROP_EMAIL_ADDRESS, g_param_spec_string ( "email-address", "Email Address", "The address from which to query config data", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property ( object_class, PROP_REGISTRY, g_param_spec_object ( "registry", "Registry", "Data source registry", E_TYPE_SOURCE_REGISTRY, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } static void e_mail_autoconfig_initable_init (GInitableIface *interface) { interface->init = mail_autoconfig_initable_init; } static void e_mail_autoconfig_init (EMailAutoconfig *autoconfig) { autoconfig->priv = E_MAIL_AUTOCONFIG_GET_PRIVATE (autoconfig); } EMailAutoconfig * e_mail_autoconfig_new_sync (ESourceRegistry *registry, const gchar *email_address, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); g_return_val_if_fail (email_address != NULL, NULL); return g_initable_new ( E_TYPE_MAIL_AUTOCONFIG, cancellable, error, "registry", registry, "email-address", email_address, NULL); } void e_mail_autoconfig_new (ESourceRegistry *registry, const gchar *email_address, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); g_return_if_fail (email_address != NULL); g_async_initable_new_async ( E_TYPE_MAIL_AUTOCONFIG, io_priority, cancellable, callback, user_data, "registry", registry, "email-address", email_address, NULL); } EMailAutoconfig * e_mail_autoconfig_finish (GAsyncResult *result, GError **error) { GObject *source_object; GObject *autoconfig; g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); source_object = g_async_result_get_source_object (result); g_return_val_if_fail (source_object != NULL, NULL); autoconfig = g_async_initable_new_finish ( G_ASYNC_INITABLE (source_object), result, error); g_object_unref (source_object); if (autoconfig == NULL) return NULL; return E_MAIL_AUTOCONFIG (autoconfig); } /** * e_mail_autoconfig_get_registry: * @autoconfig: an #EMailAutoconfig * * Returns the #ESourceRegistry passed to e_mail_autoconfig_new() or * e_mail_autoconfig_new_sync(). * * Returns: an #ESourceRegistry **/ ESourceRegistry * e_mail_autoconfig_get_registry (EMailAutoconfig *autoconfig) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), NULL); return autoconfig->priv->registry; } const gchar * e_mail_autoconfig_get_email_address (EMailAutoconfig *autoconfig) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), NULL); return autoconfig->priv->email_address; } gboolean e_mail_autoconfig_set_imap_details (EMailAutoconfig *autoconfig, ESource *imap_source) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), FALSE); g_return_val_if_fail (E_IS_SOURCE (imap_source), FALSE); return mail_autoconfig_set_details ( autoconfig, &autoconfig->priv->imap_result, imap_source, E_SOURCE_EXTENSION_MAIL_ACCOUNT); } gboolean e_mail_autoconfig_set_pop3_details (EMailAutoconfig *autoconfig, ESource *pop3_source) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), FALSE); g_return_val_if_fail (E_IS_SOURCE (pop3_source), FALSE); return mail_autoconfig_set_details ( autoconfig, &autoconfig->priv->pop3_result, pop3_source, E_SOURCE_EXTENSION_MAIL_ACCOUNT); } gboolean e_mail_autoconfig_set_smtp_details (EMailAutoconfig *autoconfig, ESource *smtp_source) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), FALSE); g_return_val_if_fail (E_IS_SOURCE (smtp_source), FALSE); return mail_autoconfig_set_details ( autoconfig, &autoconfig->priv->smtp_result, smtp_source, E_SOURCE_EXTENSION_MAIL_TRANSPORT); } void e_mail_autoconfig_dump_results (EMailAutoconfig *autoconfig) { const gchar *email_address; gboolean have_results; g_return_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig)); email_address = autoconfig->priv->email_address; have_results = autoconfig->priv->imap_result.set || autoconfig->priv->pop3_result.set || autoconfig->priv->smtp_result.set; if (have_results) { g_print ("Results for <%s>\n", email_address); if (autoconfig->priv->imap_result.set) { g_print ( "IMAP: %s@%s:%u\n", autoconfig->priv->imap_result.user, autoconfig->priv->imap_result.host, autoconfig->priv->imap_result.port); } if (autoconfig->priv->pop3_result.set) { g_print ( "POP3: %s@%s:%u\n", autoconfig->priv->pop3_result.user, autoconfig->priv->pop3_result.host, autoconfig->priv->pop3_result.port); } if (autoconfig->priv->smtp_result.set) { g_print ( "SMTP: %s@%s:%u\n", autoconfig->priv->smtp_result.user, autoconfig->priv->smtp_result.host, autoconfig->priv->smtp_result.port); } } else { g_print ("No results for <%s>\n", email_address); } }