Implementing an account store type

About

The account store is a factory type that will create account instances. For example by reading the configuration of your user. Since that configuration store will be specific to the application software, this interface makes it possible to let the application developer implement his specific code that does this.

There are a few standard account store types in libtinymail-olpc (a .ini file backend), libtinymail-gpe, libtinymail-gnome-desktop and libtinymail-maemo (these use the GConf API). It's possible that more such account store types will be made and included as an optional implementation in Tinymail.

Although the LGPL license of Tinymail does not require you to do this, you are encouraged to contribute account store implementations for your own architecture, framework or platform to the upstream Tinymail project.

Using tools/

../tools/gtypeinterface-h-files-to-c-file.pl TnyMyAccountStore \
	../libtinymail/tny-account-store.h > \
	tny-my-account-store.c

Will generate you something like this (tny-my-account-store.c)

You can remove the "event signatures"'s generated methods (the tool generates stub methods for them too). Look below for the actual methods that must be implemented.

/* Your copyright here */

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

#include <tny-my-account-store.h>
#include "tny-my-account-store-priv.h"

static GObjectClass *parent_class = NULL;


static void
tny_my_account_store_get_accounts (TnyAccountStore *self, TnyList *list, TnyGetA ccountsRequestType types)
{
}

static const gchar*
tny_my_account_store_get_cache_dir (TnyAccountStore *self)
{
        return const gchar_new ()
}

static TnyDevice*
tny_my_account_store_get_device (TnyAccountStore *self)
{
        return tny_device_new ()
}

static gboolean
tny_my_account_store_alert (TnyAccountStore *self, TnyAlertType type, const gcha r *prompt)
{
        return gboolean_new ()
}

static void
tny_my_account_store_finalize (GObject *object)
{
        parent_class->finalize (object);
}

static void
tny_account_store_init (TnyAccountStoreIface *klass)
{
        klass->get_accounts_func = tny_my_account_store_get_accounts;
        klass->get_cache_dir_func = tny_my_account_store_get_cache_dir;
        klass->get_device_func = tny_my_account_store_get_device;
        klass->alert_func = tny_my_account_store_alert;
}

static void
tny_my_account_store_class_init (TnyMyAccountStoreClass *klass)
{
        GObjectClass *object_class;

        parent_class = g_type_class_peek_parent (klass);
        object_class = (GObjectClass*) klass;
        object_class->finalize = tny_my_account_store_finalize;
}
GType
tny_my_account_store_get_type (void)
{
        static GType type = 0;
        if (G_UNLIKELY(type == 0))
        {
                static const GTypeInfo info =
                {
                        sizeof (TnyMyAccountStoreClass),
                        NULL,   /* base_init */
                        NULL,   /* base_finalize */
                        (GClassInitFunc) tny_my_account_store_class_init,   /* c lass_init */
                        NULL,   /* class_finalize */
                        NULL,   /* class_data */
                        sizeof (TnyMyAccountStore),
                        0,      /* n_preallocs */
                        tny_my_account_store_instance_init,    /* instance_init */
                        NULL
                };


                static const GInterfaceInfo tny_account_store_info =
                {
                        (GInterfaceInitFunc) tny_account_store_init, /* interfac e_init */
                        NULL,         /* interface_finalize */
                        NULL          /* interface_data */
                };

                type = g_type_register_static (G_TYPE_OBJECT,
                        "TnyMyAccountStore",
                        &info, 0);

                g_type_add_interface_static (type, TNY_ACCOUNT_STORE,
                        &tny_account_store_info);

        }
        return type;
}

Implementing the methods

tny_account_store_init

Remove the function pointers that are not needed from the generated tny_account_store_init method. For this type you'll most likely wont have to remove any.

static void
tny_account_store_init (TnyAccountStoreIface *klass)
{
        klass->get_accounts_func = tny_my_account_store_get_accounts;
        klass->get_cache_dir_func = tny_my_account_store_get_cache_dir;
        klass->get_device_func = tny_my_account_store_get_device;
        klass->alert_func = tny_my_account_store_alert;
}

tny_my_account_store_get_accounts

This method must fill the TnyList 'list' with accounts of the type 'types' (which is an enumeration that can be TNY_ACCOUNT_STORE_BOTH, TNY_ACCOUNT_STORE_TRANSPORT_ACCOUNTS or TNY_ACCOUNT_STORE_STORE_ACCOUNTS).

The per_account_get_pass_func and per_account_forget_pass_func functions in the example below must be set per created account instance. They will be called whenever the password is needed and/or when the service replied that the password was incorrect. You can take a look at the TnyPasswordGetter interface for implementing this using existing or default Tinymail components.

The reason why the second (when the password was incorrect) is called like "to forget the password", is because usually you will have a password store: the method means that you should "forget" the stored password from that store, because it's incorrect.

An implementation can, for example, show the user a dialog box with a textbox in it. For example "Give the password for ...". Implementations that use platform specific password managers are available in the platform specific Tinymail libraries. For example libtinymail-gnome-desktop has one.

If using the TnySessionCamel type for registering your accounts with, you must call (once you loaded the last initial account) the tny_session_camel_set_initialized API on it.

Before an account that is implemented in libtinymail-camel is usable, you must have used tny_camel_account_set_session (registering it). Before any account is usable, you must also have used tny_account_set_pass_func.

static gchar* 
per_account_get_pass_func (TnyAccount *account, const gchar *prompt, gboolean *cancel)
{
	return g_strdup ("unittest");
}

static void
per_account_forget_pass_func (TnyAccount *account)
{
	g_print ("Invalid test account (password was incorrect)\n");
	return;
}

static TnyAccount *account = NULL;

static void
tny_my_account_store_get_accounts (TnyAccountStore *self, TnyList *list, TnyGetAccountsRequestType types)
{
	TnyMyAccountStore *me = (TnyMyAccountStore *) self;

	if (!account)
	{
		account = TNY_ACCOUNT (tny_camel_imap_store_account_new ());

		/* Try to do this one as first */
		tny_camel_account_set_session (TNY_CAMEL_ACCOUNT (account), me->session);

		camel_session_set_online ((CamelSession*)me->session, me->force_online); 
		tny_camel_account_set_online_status (TNY_CAMEL_ACCOUNT (account), !me->force_online);
		tny_account_set_proto (account, "imap");
		tny_account_set_name (account, "unit test account");
		tny_account_set_user (account, "tinymailunittest");
		tny_account_set_hostname (account, "mail.tinymail.org");
		tny_account_set_id (account, "unique");
		tny_account_set_forget_pass_func (account, per_account_forget_pass_func);

		/* Try to do this one as last */
		tny_account_set_pass_func (account, per_account_get_pass_func);
	}

	tny_list_prepend (list, (GObject*)account);

	tny_session_camel_set_initialized (me->session);
}

tny_my_account_store_get_cache_dir

This one must return as a const char the path to the application cache. This is where E-mails will be cached and certain per-folder information stored. This can for example be a location that is mounted as a flash device's filesystem (jffs2 or LogFS or whatever makes you a happy device developer).

In case you are a filesystem developer and care about it, the writing of cache to the filesystem happens using buffered fwrite. Each time a folder is opened, a read-only mmap() of ~5 to 10 MB will happen. While a folder is being downloaded each 1000th header will that file be un-mmap()ed and rewritten (atm. from scratch) using fwrite() and then mmap()ed read-only again (until all headers are downloaded). Therefore it's possible that the file will be written up to 30 times (the 1000th header, the 2000th header, the 3000th header, etc etc). The reason for this is that else this information must stay in memory (and could possibly start consuming more memory than your device has).

In future versions of Tinymail we will most likely optimize this process not to rewrite the file from scratch, but in stead append to the file as much as possible. It's also possible that we'll split the mmap()ed file into multiple once written smaller files. There's indeed some room for improvement here.

It's probably unimportant for you unless your storage is fragile and/or can't be written often (like level-wearing on flash storage devices). For this cache directory, it's highly recommended to use LogFS and if that ain't possible jffs2 as filesystem for flash devices. On a normal desktop that utilizes a normal filesystem using a typical hard disk, please don't even care about it.

static const gchar*
tny_my_account_store_get_cache_dir (TnyAccountStore *self)
{
        return "/media/mmc1/MyEmail/.Tinymail.cache";
}

tny_my_account_store_get_device (TnyDevice)

This one must return the device instance's representative on which this account store is located. The TnyDevice device? is a singleton that notifies components in Tinymail about the connection state of the actual hardware.

static TnyDevice *the_device = NULL;
static TnyDevice*
tny_my_account_store_get_device (TnyAccountStore *self)
{
        if (!the_device)
           the_device = ...

        return the_device;
}

tny_my_account_store_alert

Display an alert with a "affirmative" and "negative" answer. Return the answer. This example implements it for Gtk+ using GtkDialog. It will be used to show, for example, SSL warnings (certificate questions).

static gboolean
tny_my_account_store_alert (TnyAccountStore *self, TnyAlertType type, const gchar *prompt)
{
	GtkMessageType gtktype;
	gboolean retval = FALSE;
	GtkWidget *dialog;

	switch (type)
	{
		case TNY_ALERT_TYPE_INFO:
		gtktype = GTK_MESSAGE_INFO;
		break;
		case TNY_ALERT_TYPE_WARNING:
		gtktype = GTK_MESSAGE_WARNING;
		break;
		case TNY_ALERT_TYPE_ERROR:
		default:
		gtktype = GTK_MESSAGE_ERROR;
		break;
	}

	dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL,
		gtktype, GTK_BUTTONS_YES_NO, prompt);

	if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES)
		retval = TRUE;

	gtk_widget_destroy (dialog);

	return retval;
}

The session instance

If you use libtinymail-camel you'll need a session instance when creating accounts. This session instance is usually created once per account store.

You will need to register each created account instance that is available in libtinymail-camel using tny_camel_account_set_session. You can for example use a priv->session in your account store's tny_account_store_get_accounts implementation.

The session in libtinymail-camel can be fed a ui locker (look at TnyLockable, TnyNoopLockable and TnyGtkLockable for more information) right after it was constructed. You use the tny_session_camel_set_ui_locker API for this:

The ui locker

A ui locker is a TnyLockable implementation that is used by Tinymail's libtinymail-camel to lock and unlock your ui toolkit. You can find some information about ui toolkit locks on this page of the GNOME wiki. There's an implementation called TnyGtkLockable that utilizes gdk_threads_enter and gdk_threads_leave for implementing the lock and unlock methods in gtk+.

The TnySessionCamel type will assume that your toolkit is in locked state at the time that you give the TnySessionCamel your locker using tny_session_camel_set_ui_locker. In other words: it will not open the door of the lock and enter it, in stead it will not nothing at first. The first time it needs the lock, it will try to enter it. There's some internal documentation about all this available: Which threads Tinymail uses, How Tinymail leverages the GDK lock, How Tinymail emits signals and fires update methods of observers, Asynchronous operations and Asynchronous connecting. Examples of when it'll need to enter are:

  • When your tny_account_store_alert implementation to be called by Tinymail
  • When the get password function is to be called by Tinymail
  • When the forget password function is to be called by Tinymail
  • When a callback of a async function is to be called by Tinymail
  • When the update of an observer is to be called by Tinymail
  • When a signal is to be emitted by Tinymail

The standard TnyAccountStore implementations (in libtinymail-gnome-desktop, libtinymail-olpc, libtinymail-maemo and libtinymail-gpe) have all been adapted to use the TnyGtkLockable as ui_locker for the TnySessionCamel instance.

#include <tny-gtk-lockable.h>

static gchar* 
per_account_get_pass_func (TnyAccount *account, const gchar *prompt, gboolean *cancel) 
{ 
    return g_strdup ("password"); 
}

static void
per_account_forget_pass_func (TnyAccount *account) { ... }

static void
tny_mygtk_account_store_get_accounts (TnyAccountStore *self, TnyList *list, TnyGetAccountsRequestType types)
{
    TnyMyGtkAccountStorePriv *priv = TNY_MYGTK_ACCOUNT_STORE_GET_PRIVATE (self);

    for each account in configuration:
    {
        TnyCamelAccount *account = tny_camel_[imap|pop|...]_account_new ();

	/* Try to do this one as first */
        tny_camel_account_set_session (account, priv->session);

        ...

	tny_account_set_forget_pass_func (TNY_ACCOUNT (account),
		per_account_forget_pass_func);

	/* Try to do this one as last */
	tny_account_set_pass_func (TNY_ACCOUNT (account),
		per_account_get_pass_func);

        tny_list_prepend (list, G_OBJECT (account));
        g_object_unref (G_OBJECT (account));
    }

    return;
}

/**
 * tny_mygtk_account_store_new:
 *
 *
 * Return value: A new #TnyAccountStore instance for gtk+ with libtinymail-camel
 **/
TnyAccountStore*
tny_mygtk_account_store_new (void)
{
	TnyMyGtkAccountStore *self = g_object_new (TNY_TYPE_MYGTK_ACCOUNT_STORE, NULL);
	TnyMyGtkAccountStorePriv *priv = TNY_MYGTK_ACCOUNT_STORE_GET_PRIVATE (self);

	priv->session = tny_session_camel_new (TNY_ACCOUNT_STORE (self));
	tny_session_camel_set_ui_locker (priv->session, tny_gtk_lockable_new ());

	return TNY_ACCOUNT_STORE (self);
}

The public .h file (tny-my-account-store.h)

#ifndef TNY_MY_ACCOUNT_STORE_H
#define TNY_MY_ACCOUNT_STORE_H


#include <glib.h>
#include <glib-object.h>
#include <tny-shared.h>

#include <tny-camel-shared.h>

G_BEGIN_DECLS

#define TNY_TYPE_MY_ACCOUNT_STORE \
	(tny_my_account_store_get_type ())
#define TNY_MY_ACCOUNT_STORE(obj) \
	(G_TYPE_CHECK_INSTANCE_CAST ((obj), TNY_TYPE_MY_ACCOUNT_STORE, TnyMyAccountStore))
#define TNY_MY_ACCOUNT_STORE_CLASS(vtable) \
	(G_TYPE_CHECK_CLASS_CAST ((vtable), TNY_TYPE_MY_ACCOUNT_STORE, TnyMyAccountStoreClass))
#define TNY_IS_MY_ACCOUNT_STORE(obj) \
	(G_TYPE_CHECK_INSTANCE_TYPE ((obj), TNY_TYPE_MY_ACCOUNT_STORE))
#define TNY_IS_MY_ACCOUNT_STORE_CLASS(vtable) (G_TYPE_CHECK_CLASS_TYPE ((vtable), TNY_TYPE_MY_ACCOUNT_STORE))
#define TNY_MY_ACCOUNT_STORE_GET_CLASS(inst) \
	(G_TYPE_INSTANCE_GET_CLASS ((inst), TNY_TYPE_MY_ACCOUNT_STORE, TnyMyAccountStoreClass))

typedef struct _TnyMyAccountStore TnyMyAccountStore;
typedef struct _TnyMyAccountStoreClass TnyMyAccountStoreClass;

struct _TnyMyAccountStore
{
	GObject parent;
};

struct _TnyMyAccountStoreClass
{
	GObjectClass parent;
};

GType tny_my_account_store_get_type (void);
TnyAccountStore* tny_my_account_store_new (void);
TnySessionCamel* tny_my_account_store_get_session (TnyMyAccountStore *self);

G_END_DECLS

#endif

Adjusting Makefile.am

libtinymail_my_1_0_headers = \
+       tny-my-account-store.h \
        tny-my-password-dialog.h \
        tny-my-device.h \
        tny-my-platform-factory.h

libtinymail_my_1_0_la_SOURCES = \
        $(libtinymail_my_1_0_headers) \
+       tny-my-account-store.c \
        tny-my-device-priv.h \
        tny-my-device.c \
        tny-my-password-dialog.c \
        tny-my-platform-factory.c

References to related other documentation