/* * 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; either * version 2 of the License, or (at your option) version 3. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see * */ #include "e-mail-autoconfig.h" #include #include #include /* Stuff for DNS querying and message parsing. */ #include #include #include #include #if defined(HAVE_ARPA_NAMESER_COMPAT_H) && !defined(GETSHORT) #include #endif /* 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/" /* XXX g_file_load_contents() on an "http://" URI returns error codes * in the SOUP_HTTP_ERROR domain instead of the G_IO_ERROR domain. * That is both undocumented and unexpected. */ #define ERROR_IS_NOT_FOUND(error) \ (g_error_matches ((error), SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) typedef struct _ParserClosure ParserClosure; typedef struct _ResolverClosure ResolverClosure; struct _EMailAutoconfigPrivate { gchar *email_address; gchar *email_local_part; gchar *email_domain_part; gchar *markup_content; }; struct _ParserClosure { CamelNetworkSettings *network_settings; const gchar *expected_type; const gchar *email_address; const gchar *email_local_part; const gchar *email_domain_part; gboolean in_server_element; gboolean settings_modified; }; struct _ResolverClosure { volatile gint ref_count; GMainContext *main_context; GMainLoop *main_loop; gchar *domain_name; gchar *name_server; GError *error; }; enum { PROP_0, PROP_EMAIL_ADDRESS }; /* 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 ResolverClosure * resolver_closure_new (const gchar *domain_name) { ResolverClosure *closure; closure = g_slice_new0 (ResolverClosure); closure->domain_name = g_strdup (domain_name); closure->main_context = g_main_context_new (); closure->main_loop = g_main_loop_new (closure->main_context, FALSE); closure->ref_count = 1; return closure; } static ResolverClosure * resolver_closure_ref (ResolverClosure *closure) { g_return_val_if_fail (closure != NULL, NULL); g_return_val_if_fail (closure->ref_count > 0, NULL); g_atomic_int_inc (&closure->ref_count); return closure; } static void resolver_closure_unref (ResolverClosure *closure) { g_return_if_fail (closure != NULL); g_return_if_fail (closure->ref_count > 0); if (g_atomic_int_dec_and_test (&closure->ref_count)) { g_main_context_unref (closure->main_context); g_main_loop_unref (closure->main_loop); g_free (closure->domain_name); g_free (closure->name_server); g_clear_error (&closure->error); g_slice_free (ResolverClosure, closure); } } static gboolean mail_autoconfig_resolver_idle_quit (gpointer user_data) { GMainLoop *main_loop = user_data; g_main_loop_quit (main_loop); return FALSE; } static void mail_autoconfig_resolver_cancelled (GCancellable *cancellable, ResolverClosure *closure) { GSource *source; source = g_idle_source_new (); g_source_set_callback ( source, mail_autoconfig_resolver_idle_quit, g_main_loop_ref (closure->main_loop), (GDestroyNotify) g_main_loop_unref); g_source_attach (source, closure->main_context); g_source_unref (source); } static gpointer mail_autoconfig_resolver_thread (gpointer user_data) { ResolverClosure *closure = user_data; HEADER *header; guchar answer[1024]; gchar namebuf[1024]; guchar *end, *cp; gint count; gint length; gint herr; /* Query DNS for the MX record for the domain name given in the * email address. We need an authoritative name server for it. */ length = res_query ( closure->domain_name, C_IN, T_MX, answer, sizeof (answer)); herr = h_errno; /* h_errno is defined in */ /* Based heavily on _g_resolver_targets_from_res_query(). * The binary DNS message format is described in RFC 1035. */ if (length <= 0) { if (length == 0 || herr == HOST_NOT_FOUND || herr == NO_DATA) g_set_error ( &closure->error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND, _("No mail exchanger record for '%s'"), closure->domain_name); else if (herr == TRY_AGAIN) g_set_error ( &closure->error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_TEMPORARY_FAILURE, _("Temporarily unable to resolve '%s'"), closure->domain_name); else g_set_error ( &closure->error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, _("Error resolving '%s'"), closure->domain_name); goto exit; } header = (HEADER *) answer; cp = answer + sizeof (HEADER); end = answer + length; /* Skip the 'question' section. */ count = ntohs (header->qdcount); while (count-- && cp < end) { cp += dn_expand (answer, end, cp, namebuf, sizeof (namebuf)); cp += 2; /* skip QTYPE */ cp += 2; /* skip QCLASS */ } /* Skip the 'answers' section. */ count = ntohs (header->ancount); while (count-- && cp < end) { guint16 rdlength; cp += dn_expand (answer, end, cp, namebuf, sizeof (namebuf)); cp += 2; /* skip TYPE */ cp += 2; /* skip CLASS */ cp += 4; /* skip TTL */ GETSHORT (rdlength, cp); /* read RDLENGTH */ cp += rdlength; /* skip RDATA */ } /* Read the 'authority' section. */ count = ntohs (header->nscount); while (count-- && cp < end) { guint16 type, qclass, rdlength; cp += dn_expand (answer, end, cp, namebuf, sizeof (namebuf)); GETSHORT (type, cp); GETSHORT (qclass, cp); cp += 4; /* skip TTL */ GETSHORT (rdlength, cp); if (type != T_NS || qclass != C_IN) { cp += rdlength; continue; } cp += dn_expand (answer, end, cp, namebuf, sizeof (namebuf)); /* Pick the first T_NS record we find. */ closure->name_server = g_strdup (namebuf); break; } if (closure->name_server == NULL) g_set_error ( &closure->error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND, _("No authoritative name server for '%s'"), closure->domain_name); exit: g_main_loop_quit (closure->main_loop); resolver_closure_unref (closure); return NULL; /* return value is not used */ } static gchar * mail_autoconfig_resolve_authority (const gchar *domain, GCancellable *cancellable, GError **error) { ResolverClosure *closure; GThread *resolver_thread; gchar *name_server = NULL; gulong cancel_id = 0; closure = resolver_closure_new (domain); /* DNS record lookup is not cancellable, so we run it in a * separate thread. We don't join with the thread, however, * because if we get cancelled we want to return immediately. * So use a reference count on the thread closure and always * let the thread run to completion even if we're not around * any longer to pick up the result. */ resolver_thread = g_thread_create ( mail_autoconfig_resolver_thread, resolver_closure_ref (closure), FALSE /* not joinable */, error); if (resolver_thread == NULL) return FALSE; if (G_IS_CANCELLABLE (cancellable)) cancel_id = g_cancellable_connect ( cancellable, G_CALLBACK (mail_autoconfig_resolver_cancelled), resolver_closure_ref (closure), (GDestroyNotify) resolver_closure_unref); g_main_loop_run (closure->main_loop); if (cancel_id > 0) g_cancellable_disconnect (cancellable, cancel_id); if (g_cancellable_set_error_if_cancelled (cancellable, error)) { /* do nothing */ } else if (closure->error != NULL) { g_warn_if_fail (closure->name_server == NULL); g_propagate_error (error, closure->error); closure->error = NULL; } else { g_warn_if_fail (closure->name_server != NULL); name_server = closure->name_server; closure->name_server = NULL; } resolver_closure_unref (closure); return name_server; } static gboolean mail_autoconfig_lookup (EMailAutoconfig *autoconfig, const gchar *domain, GCancellable *cancellable, GError **error) { GFile *file; gchar *uri; gboolean success; uri = g_strconcat (AUTOCONFIG_BASE_URI, domain, NULL); file = g_file_new_for_uri (uri); g_free (uri); /* Just to make sure we don't leak. */ g_free (autoconfig->priv->markup_content); autoconfig->priv->markup_content = NULL; success = g_file_load_contents ( file, cancellable, &autoconfig->priv->markup_content, NULL, NULL, error); g_object_unref (file); return success; } 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; 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) { 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); closure->in_server_element = (g_strcmp0 (type, closure->expected_type) == 0); } } 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->in_server_element = FALSE; } static void mail_autoconfig_parse_text (GMarkupParseContext *context, const gchar *text, gsize text_length, gpointer user_data, GError **error) { ParserClosure *closure = user_data; const gchar *element_name; GString *string; if (!closure->in_server_element) 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 = closure->email_address; if (strncmp (cp, variable, strlen (variable)) == 0) { g_string_append (string, substitute); cp += strlen (variable); continue; } variable = "%EMAILLOCALPART%"; substitute = closure->email_local_part; if (strncmp (cp, variable, strlen (variable)) == 0) { g_string_append (string, substitute); cp += strlen (variable); continue; } variable = "%EMAILDOMAIN%"; substitute = closure->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")) { camel_network_settings_set_host ( closure->network_settings, string->str); closure->settings_modified = TRUE; } else if (g_str_equal (element_name, "username")) { camel_network_settings_set_user ( closure->network_settings, string->str); closure->settings_modified = TRUE; } else if (g_str_equal (element_name, "port")) { glong port = strtol (string->str, NULL, 10); if (port == CLAMP (port, 1, G_MAXUINT16)) { camel_network_settings_set_port ( closure->network_settings, (guint16) port); closure->settings_modified = TRUE; } } else if (g_str_equal (element_name, "socketType")) { if (g_str_equal (string->str, "plain")) { camel_network_settings_set_security_method ( closure->network_settings, CAMEL_NETWORK_SECURITY_METHOD_NONE); closure->settings_modified = TRUE; } else if (g_str_equal (string->str, "SSL")) { camel_network_settings_set_security_method ( closure->network_settings, CAMEL_NETWORK_SECURITY_METHOD_SSL_ON_ALTERNATE_PORT); closure->settings_modified = TRUE; } else if (g_str_equal (string->str, "STARTTLS")) { camel_network_settings_set_security_method ( closure->network_settings, CAMEL_NETWORK_SECURITY_METHOD_STARTTLS_ON_STANDARD_PORT); closure->settings_modified = TRUE; } } /* FIXME Not handling elements. * Unclear how some map to SASL mechanisms. */ 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 gboolean mail_autoconfig_set_details (EMailAutoconfig *autoconfig, const gchar *expected_type, ESource *source, const gchar *extension_name) { GMarkupParseContext *context; ESourceCamel *camel_ext; ESourceBackend *backend_ext; CamelSettings *settings; ParserClosure closure; const gchar *backend_name; const gchar *markup_content; gboolean success; 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); markup_content = e_mail_autoconfig_get_markup_content (autoconfig); g_return_val_if_fail (markup_content != NULL, FALSE); closure.network_settings = CAMEL_NETWORK_SETTINGS (settings); closure.expected_type = expected_type; closure.in_server_element = FALSE; closure.settings_modified = FALSE; /* These are used for text substitutions. */ closure.email_address = autoconfig->priv->email_address; closure.email_local_part = autoconfig->priv->email_local_part; closure.email_domain_part = autoconfig->priv->email_domain_part; context = g_markup_parse_context_new ( &mail_autoconfig_parser, 0, &closure, (GDestroyNotify) NULL); success = g_markup_parse_context_parse ( context, markup_content, strlen (markup_content), NULL); success &= g_markup_parse_context_end_parse (context, NULL); /* Did we actually configure anything? */ success &= closure.settings_modified; g_markup_parse_context_free (context); return success; } 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_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; } 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; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } 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->markup_content); /* 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 "mail exchanger" (MX) DNS record. */ name_server = mail_autoconfig_resolve_authority ( 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->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)); } 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 (const gchar *email_address, GCancellable *cancellable, GError **error) { g_return_val_if_fail (email_address != NULL, NULL); return g_initable_new ( E_TYPE_MAIL_AUTOCONFIG, cancellable, error, "email-address", email_address, NULL); } void e_mail_autoconfig_new (const gchar *email_address, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (email_address != NULL); g_async_initable_new_async ( E_TYPE_MAIL_AUTOCONFIG, io_priority, cancellable, callback, user_data, "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); } 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; } const gchar * e_mail_autoconfig_get_markup_content (EMailAutoconfig *autoconfig) { g_return_val_if_fail (E_IS_MAIL_AUTOCONFIG (autoconfig), NULL); return autoconfig->priv->markup_content; } 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, "imap", 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, "pop3", 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, "smtp", smtp_source, E_SOURCE_EXTENSION_MAIL_TRANSPORT); }