aboutsummaryrefslogblamecommitdiffstats
path: root/mail/e-mail-autoconfig.c
blob: aeebb0fa22d9498fe75e81985e0ec00f23adfb01 (plain) (tree)























                                                                             


                         






                                                                



                                                                           

















                                               


















                                                                            
              


                                                               
 

                            
                                  
 
                                             
 

                                                                            
 



                                                                      

         


                                                                     



                           






                                                                 





                                                    



                                  
                   

                                                

                                                              
                                                               

                     





                                                                           
 

















                                                                        
 

                                      
 
                                                  






















































































































































































































































































































































                                                                                         

                                                                    

















































































































































































                                                                          
/*
 * 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 <http://www.gnu.org/licenses/>
 *
 */

#include "e-mail-autoconfig.h"

#include <config.h>
#include <string.h>
#include <glib/gi18n-lib.h>

/* For error codes. */
#include <libsoup/soup.h>

#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 _ParserClosure ParserClosure;

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;
};

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 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)
{
    SoupMessage *soup_message;
    SoupSession *soup_session;
    gulong cancel_id = 0;
    guint status;
    gchar *uri;

    soup_session = soup_session_sync_new ();

    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);

    if (SOUP_STATUS_IS_SUCCESSFUL (status)) {

        /* Just to make sure we don't leak. */
        g_free (autoconfig->priv->markup_content);

        autoconfig->priv->markup_content =
            g_strdup (soup_message->response_body->data);
    } else {
        g_set_error_literal (
            error, SOUP_HTTP_ERROR,
            soup_message->status_code,
            soup_message->reason_phrase);
    }

    g_object_unref (soup_message);
    g_object_unref (soup_session);

    return SOUP_STATUS_IS_SUCCESSFUL (status);
}

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 <authentication> 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 "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->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);
}