haiku/docs/apps/mail/Programming Notes/Writing Add-ons.md
Augustin Cavalier cba277f2f9 docs/apps/mail: Extract the "Writing Add-ons" guide and convert to markdown.
I guess it was at one point in Be Styled Text format, but it didn't
extract that way for me, so I had to manually recreate the styling...
2017-12-25 17:15:21 -05:00

15 KiB
Raw Blame History

About this Document (Discouragement for Cheaters)

You need to read the whole thing. If you are writing a protocol, you have to understand filters, chains, callbacks, the works. Filter authors can skip the "Writing a Protocol" section, I suppose, but other than that, read it all. Trust me, it's worth it. Also, if something doesn't make sense, or at any point something becomes difficult that you feel probably shouldn't be, drop us a line at zoidberg@bug-br.org.br  we're glad to help.

General Architecture - Introduction to Chains

When you create an account in E-mail preferences, it creates two chains: one inbound, one outbound. Each chain consists of a number of stored references to mail filters, the generic type of Mail Daemon add-on, and their settings, in the form of a flattened BMessage. The chain also stores global chain meta data, also in a flattened BMessage, and various auxiliary information like the chain name, and whether it is an outbound or inbound chain. The distinction is important only to (1) set the color of the status bar when the chain is run and (2) identify to the daemon which chains to run when it is asked to fetch or to send mail. Each chain is identified by a unique unsigned 32-bit integer, the chain ID.

Introduction to Filters

Every MDR add-on is conceptually a filter, and, programmatically, derived from the Mail::Filter class (which is to be found in MailAddon.h). An MDR filter is not the standard sort of e-mail filter (a sorter, etc.), but is defined to be any sort of entity that modifies, parses, or otherwise cares about an e-mail message in the process of it being sent (or received). Into this very broad definition, it is possible to fit all the add-ons it is possible (for us, at any rate, with our inadequate minds) to imagine: protocols, standard e-mail filters, notification windows, message saving, etc. In fact, the vast, vast majority of what happens when a message is transferred happens not in the daemon itself, but in one of its many add-ons.

Introduction to ChainRunner and Callbacks

The Mail::ChainRunner class exists, as the name would imply, to run chains. It is of great use to you, the MDR add-on author. It publishes a variety of useful public routines (like ShowError()) that will be described in their appropriate sections, and does a number of other things that will also be described later. But it does do one thing that is of general importance and interest, and as near in importance for you to understand as filters: callbacks.

Callbacks are called at the completion (successful or otherwise) of some aspect of chain execution.

At the moment of completion, the callback's Callback() routine is called with the error code that completed whatever it is the callback was waiting for (B_OK or one of the B_MAIL_* family in MailAddon.h generally indicate successful completion), and the callback is then destroyed. Callbacks come in three kinds: message, process, and chain, and are registered by the Register*Callback() routines of ChainRunner. These types of callbacks are called, respectively, at the termination of a message transfer, a block of message transfers (e.g. after all new messages are fetched off the server), or the chain (just before all the add-ons are to be destroyed). These are useful for a whole variety of tasks, and are used, for example, in such things as deleting messages after they are fetched in POP3.

How to Write a Filter

The Mail::Filter class has two important hooks (actually, it only has two hooks, but they are quite important one): InitCheck() and ProcessMailMessage(). InitCheck() corresponds to the standard Be API InitCheck() function: after construction of your filter (which, for things like protocols, may involve complicated things like connecting to a server), InitCheck() is called. If something is wrong (say, you couldn't connect to the server), return an appropriate error code, and, if out_message does not equal NULL, set it to an appropriate human readable error message. If it does equal NULL, it is suggested that you call ChainRunner's ShowError() routine (see Error Reporting for more information). If InitCheck() returns an error, construction of the chain stops, all filters are deleted, and ChainRunner packs up and goes home.

After successful construction of all the filters in the chain, ProcessMailMessage() is called for each message that passes through it. It takes what looks, at first glance, like a bewildering array of arguments, but they generally make sense and most filter applications don't need to use them all anyway.

  • BPositionIO** io_message: This is where the message is to be written to (or read from). Astute observers will note that it is a pointer to a pointer, and will question either our sanity or my typing, depending on their frames of mind and personalities, among other things. But this aspect allows you to modify the argument in unexpected (and, naturally, very useful) ways. IMAP and POP3 replace the argument with their own reader that retrieves data from the server as it is requested. The Outbox filter swaps the argument for a BFile pointing to the message to be fetched. Note that if you do swap it, you become responsible for the deletion of the old argument. If you don't, there will be memory leaks and other untold havoc. (Further information on replacing io_message is available under How to Write a Protocol)

  • BEntry *io_entry: This tells you where, on disk, the contents of the message are kept. Useful for debugging purposes and for moving it about, although this last is not reccomended. For information on why not, see io_headers and io_folder (the next two, for the lazy).

  • BMessage *io_headers: This contains a list of various kinds of random junk in addition to a list of the headers of the message (after it's been through the Parser filter, which means it's blank for protocols, and full of yummy data for everyone else). The headers are stored as strings, with the key the header tag in whatever case it was in the message header (the subject, for instance, can be found with FindString("Subject")). If you modify these entries, they are written to disk in whatever form you leave them. In addition, there are several MDR-added entries (the previously mentioned "random junk"). These are the THREAD, NAME, SIZE, and DESTINATION fields. THREAD is the message thread (the subject with, Re:, Fwd:, etc. removed), NAME is the name of the sender (as displayed in Tracker in the "Name" attribute), SIZE (stored as a size_t) is the complete message size (in bytes), and DESTINATION, which may or may not have been added, is an override value for where, on disk, the message should be stored. You can add this to have the message be placed somewhere other than the user's defined inbox.

  • BPath *io_folder: This defines the subfolder of the user's inbox to which the message will be added, expressed relative to the inbox. IMAP uses it for the folder structure (it sets it to the name of the IMAP folder), and POP3 leaves it blank. If left blank, it will not be placed in a subfolder.

  • const char *io_uid: This is the unique id of the message, in some form that makes sense to the protocol. Usually of no concern to any filter.

After processing the message, ProcessMailMessage() returns either B_OK, a descriptive error code, or one of the constants at the top of MailAddon.h (B_MAIL_DISCARD, B_MAIL_END_FETCH, or B_MAIL_END_CHAIN). B_OK causes the message to continue down the chain, B_MAIL_DISCARD causes it to be deleted from disk and from the server and terminates the processing of the message, error codes terminates the processing of the message as well, B_MAIL_END_FETCH terminates the fetching of all remaining messages in this fetch block, and B_MAIL_END_CHAIN indicates a catastrophic error has occurred that requires the chain to be destroyed and the connection closed.

Instantiating and Configuring the Filter

MDR uses three symbols in a filter, two of which are optional. They are described below:

instantiate_mailfilter: This is called to instantiate a new copy of your filter. It is passed a copy of the filter's settings and a pointer to the calling ChainRunner.

instantiate_config_panel: This is passed a copy of your filter's settings and the chain meta data. From it, you should return a BView with configuration options. E-mail prefs will call ResizeToPreferred() on it after it is instantiated. To save, the prefs app will call Archive(). The passed BMessage * becomes your settings.

descriptive_name: This is passed the settings of the filter, and a char * buffer. If this routine returns B_OK, the contents of the buffer will replace the name of the add-on in E-mail prefs.

How to Write a Protocol

While it is possible to write a protocol using nothing but the Mail::Filter hooks, this is the Bad Way™ to do it. Instead of forcing you through that, we've created the spectacularly useful Mail::Protocol class (found, unsurprisingly, in MailProtocol.h). Mail::Protocol has two hooks, GetMessage() and DeleteMessage(), a few member items, and a number of important conventions. The MDR side of a mail protocol is fairly simple and easy to understand; the network side of things may not be, and the best we can do there is wish you luck. But you (hopefully) won't be cursing MDR.

Part I: Starting the Connection (or, what to do in your constructor)

When your protocol is instantiated by instantiate_mailfilter(), you are expected to initiate the connection. Information on this is contained in your settings in a standard format, and can be written to your settings in that format by Mail::ProtocolConfigView. The existance of this class makes your life easy (you can return one from instantiate_config_panel and not worry about configuration any further). The format is described at the end of this section. After successfully establishing the connection, you are expected to add the unique ids of every message on the server to the protected data member unique_ids. This is a StringList, a special class we've created just for MDR. It uses simple operators like +=, and shouldn't require too much work to understand. The header is StringList.h, in the support subdirectory. After adding all the unique ids, you need to tell ChainRunner to get the new messages. You do this as follows:

StringList to_dl;
manifest->NotHere(*unique_ids, &to_dl);
runner->GetMessages(&to_dl, maildrop_size);

where maildrop_size is the combined total length (in bytes) of all the messages on the server. If you don't know this, or determining it would be complicated, slow, awkward, or just plain annoying, you can pass -1, in which case the status bar will advance by message count instead of transferred bytes.

Part II: Protocol Settings Format

server (string): The IP address or hostname of the server

port (int32): The port on the server to connect to, if the user has specified one

flavor (int32): The 0-based index of the protocol flavor the user has chosen. If you didn't give the
		user a choice of flavors in ProtocolConfigView, you can ignore this with impunity.

username (string): The user name entered in config.

password & cpasswd (string): These give you the password, which may or may not have been stored
		encrypted. Use this code to get the password in plain text (stored in the password variable):

	const char *password = settings->FindString("password");
	char *passwd = get_passwd(settings, "cpasswd");
	if (passwd)
		password = passwd;

auth_method (int32): The 0-based index of the authentication method the user has chosen. If you
		didn't give the user a choice of methods in ProtocolConfigView, you can ignore this with
		impunity.

Part III: Fetching Messages (or, what to do in GetMessage())

In your protocol's GetMessage() routine, you fetch the message indicated by uid, into out_file. If your protocol is of the type that has multiple folders, you can indicate that to future filters by setting out_folder_location to the name of the folder in which the message is found. That's all you need to do.

Things get more complicated (you knew they would) if you want to support partial message downloading. To do this, you need to replace out_file with some sort of BPositionIO derivative that reads the message as required. Every byte read from your BPositionIO derivative must also be written in the on-disk representation of the message, that is, the old out_file argument. Second, when your sub-class is deleted, you must delete the old out_file. Third, when anyone does a Seek() operation referenced from SEEK_END, you must download the whole message. You also need to add to out_headers an int32 named SIZE containing the size, in bytes, of the complete message.

Part IV: Deleting Messages

This is really simple. When DeleteMessage() is called, you delete the message indicated by uid. You also need to modify the unique_ids list. To do this, just do (*unique_ids) -= uid;.

Part V: The Rest of It

As far as MDR is concerned, there is no rest of it. Everything else on the BeOS side of things is taken care of by Mail::Protocol. Then there's the network.... we'll leave you to that, and bother you no further, except to ask you to read the next two sections:

RemoteStorageProtocol

For IMAP-like protocols (that is, remotely stored mail systems with multiple mailboxes), we provide you with the RemoteStorageProtocol class. It handles most everything on the BeOS side. You need simply to implement RSP's six hook functions: GetMessage(), AddMessage(), DeleteMessage(), CopyMessage(), CreateMailbox(), and DeleteMailbox(). These should be fairly self-explanatory. A couple of notes are in order, however.

First, unique ids MUST NOT contain a '/' character. If they do, everything will go to hell.

Second, when you fill the unique_ids structure, use the following format: mailbox/id. Mailbox names can contain / characters, and foo/bar will be interpreted as a nested directory.

Second, you needn't remove anything from unique_ids in DeleteMessage(). That's handled for you.

Third, hooks like CopyMessage() and AddMessage() are passed the unique id in a BString pointer. Fill this with the unique id the copy/uploaded message receives once it's on the server.

Progress Reporting

When your protocol receives a message, or a part of it, it is important (from the user's point of view) that you display that fact. ChainRunner provides a very simple way to do this, in its ReportProgress() function. ReportProgress() takes three arguments: the number of bytes received since the last call, the number of messages received since the last call, and an update message. If you just want to inform the user of something happening ("Logging in", for instance), you can leave the bytes and messages argument blank.

Error Reporting

To report an error to the user, you need to call ChainRunner's ShowError() method with some human-readable string describing the error condition. In addition, you should take whatever action is necessary to report the error to MDR in machine-understandable form, such as returning an error code, or calling ChainRunner's Stop() method. Note that Stop() adds a message to the end of the queue you need to return a fatal error from ProcessMailMessage() to interrupt a mail fetch in progress.