My activities New request

contact atmail support

PH: +61 (7) 5357-6605

support@atmail.com

Follow

Creating Plugins

Stewart -

PROBLEM

How can I create my own plugins for use with my Atmail.

ENVIRONMENT

  • On-Premise Server + WebMail Installations: Version 6.0 > 7.8
  • Webmail Only Installations: Version 6.0 > 7.8

CAUSE

In this document we will go through a step by step procedure for creating a plugin. We'll use a real life example, the Barracuda4Atmail plugin, to help make things easier to follow.

RESOLUTION

Establish Your Plugin Development Directory

First up we need to create a specific directory structure for our plugin development. You can choose any location on your filesystem, lets use /Users/brad/AtmailPlugins for this example. So first up I create my plugin development directory:

mkdir /User/brad/AtmailPlugins

Now /Users/brad/AtmailPlugins is the storage location for all my plugin sources, I'll refer to this location as PluginDevRoot. I now need to create another directory within my PluginDevRoot, this directory should use a name that is likely to be unique among all other plugin developers. We call it the CompanyName folder so it makes sense for you to use your company/business name if applicable. If you're not developing a plugin on behalf of a company then use your domain name (minus any non alphanumeric characters) or some other alphanumeric string that is highly likely to be unique.

I'll go off on a bit of a tangent here and explain why a unique CompanyName folder is important: The reason for this is that plugins are installed into Atmail under a path like plugins/Company-Name/Plugin-Name/, this is done to help avoid any name conflicts with plugins. So if your CompanyName folder is the same as someone elses, then when your plugin package is installed into Atmail it could be extracted in another developer's plugin directory and if by chance your plugin shares the same name as one already installed and created by that developer then it will overwrite the existing plugin. For example, I create a CompanyName folder called FooBar and create a plugin called HelloWorld under it. A user installs my plugin but already has a plugin called HelloWorld from a developer that also used the company name FooBar. So your plugin will be extracted into the same path as the original plugin, overwriting it.

Ok so I'm working for Atmail and it's likely to be a unique name for a CompanyName folder so I'll use it:

mkdir /User/brad/AtmailPlugins/Atmail

So I now have my "PluginDevRoot" folder and my "CompanyName" folder. Now it's time to create the folder for this particular plugin I am working on, the Barracuda4Atmail plugin:

mkdir /User/brad/AtmailPlugins/Atmail/Barracuda4Atmail

Also note that both the CompanyName and PluginName folders must only contain characters that are legal for PHP class names.

Create the Plugin File

Now it's time to create the actual plugin file. In the CLI example below I use the vim editor, but of course you can use whatever text editor you like - CLI or GUI - just create a file called Plugin.php under your PluginName folder: /User/brad/AtmailPlugins/Atmail/Barracuda4Atmail/Plugin.php in this example.

vim /User/brad/AtmailPlugins/Atmail/Barracuda4Atmail/Plugin.php

Now you need to use a specific class name for your plugin. All plugin names reflect their path within your PluginDevRoot folder. So for this example:

Path = Atmail/Barracuda4Atmail/Plugin.php

Class Name = Atmail_Barracuda4Atmail_Plugin

So I edit Plugin.php:

class Atmail_Barracuda4Atmail_Plugin extends Atmail_Controller_Plugin
{

As you can see I have extended from the class Atmail_Controller_Plugin, all plugins must extend from this class.

Now we add our plugin information:

protected $_pluginFullName   = 'Barracuda4Atmail';
protected $_pluginCompany = 'Atmail';
protected $_pluginAuthor = 'Brad Kowalczyk brad@staff.atmail.com';   
protected $_pluginCopyright = 'Brad Kowalczyk brad@staff.atmail.com';
protected $_pluginUrl = '';
protected $_pluginNotes = '';
protected $_pluginVersion = '1.0.0';
protected $_pluginModule = 'mail';
protected $_pluginCompat = '6.1.6 6.1.7';
  
protected $_pluginImplements = array('anti-spam');

Most of the class properties listed above are pretty much self-explanatory but here is a run-down:

$_pluginFullName The name of your plugin (REQUIRED)
$_pluginCompany Your CompanyName
$_pluginAuthor The plugin author's name
$_pluginDescription A description of the plugin, what it does etc.
$_pluginCopyright Assign any copyright here
$_pluginUrl URL for any web site you may have for the plugin
$_pluginNotes Any other notes you may wish to add (changelog for eg)
$_pluginVersion The current version for this plugin
$_pluginModule The Atmail module for which the plugin is designed. Can be one of 'mail', 'admin' or 'api'. (REQUIRED)
$_pluginCompat A space separated list of Atmail versions that this plugin is known to work with
$_pluginImplements An array of features the plugin implements - at this time only "anti-spam" is a valid item for this array. This is used by Atmail to skip certain core functionality if a plugin implements a feature.

Now we need to implement the methods for the events we want to catch (see the Appendix at the end of this document for a full listing of events).

public function __construct()
{
parent::__construct();
  
$this->_pluginDescription = "Adds Barracuda Spam FW integration for Atmail. After installation at a shell execute: \"cd " . APP_ROOT . "application/modules/mail/plugins/Atmail/Barracuda4Atmail/
 ; mv config.ini.dist config.ini\" Then edit config.ini so it has your
correct Barracuda API details";
}
  
  
public function preDispatch()
{
$conf = Zend_Registry::get('config')->exim;
  
if ($conf['filter_sa_enable'] == 1) {
config::save('exim', array('filter_sa_enable' => 0));
}
}

First up I use a constructor so I can create $this->_pluginDescription using the APP_ROOT constant, this means I must also call parent::__construct(). Whenever you implement __construct() in a plugin you must also remember to call parent::__construct().

Then I've implemented the preDispatch() method, this catches the preDispatch event (I use the term "event" rather loosely) which is triggered just before the Zend Framework (ZF) dispatches the request to the specified controller for processing. I catch this event so I can check if the core SpamAssassin module is turned off and if it's still on I turn it off as I presume the admin installed this plugin to interface with their Barracuda Spam Firewall and don't want to use the bundled SpamAssassin.

Next I've implemented the setup() method, this method is called on all plugins ONLY when they are installed. If you have some install time modifications or setup required (such as creating a new DB table for eg) then you should implement this function to achieve what you need to. I've simply used it to turn off the bundled SpamAssassin in Atmail:

public function setup()
{
config::save('exim', array('filter_sa_enable' => 0));
}

Now I want to have Atmail display a navigation button under the Settings tab in Atmail:

public function renderMailSettingsNav()
{
$params = array(
"title" => "Barracuda Spam FW Settings",
"text"  => "Spam Firewall",
"icon"  => "images/spam-settings.png"
);
  
echo $this->_createMailSettingsNavSection($params);
}

The renderMailSettingsNav() method catches the renderMailSettingsNav event that is triggered after the default navigation items are rendered to the webmail Settings page. I implement this method to display the button for this plugin's settings. The _createMailSettingsNavSection() method I use is defined in Atmail_Controller_Plugin and acts as a helper method to allow plugin authors to easily create a navigation button for the webmail settings page. The array I pass this function defines arguments for creating the navigation section:

  • "title" - sets the "title" attribute for the tag
  • "text" - sets the label text for the button
  • "icon" - sets the icon to use on the button (must be a path relative to where Plugin.php is located)

Note the "Spam Firewall" button now in the navigation pane:

Next I implement the method called settings(), this method should be implemented to display any settings page your plugin may have. It is linked to automatically by the navigation button created when you use _createMailSettingsNavSection() to create the navigation button for the webmail settings page. The button calls this method via the Plugininterface controller rather than via a triggered event.

public function settings()
{
$this->_initView();

In the above settings() method I first setup views for the plugin by calling $this->_initView(). This allows me to use the $this->view object (Zend_View) for displaying pages (views) to the browser. Your view files (html templates) should be stored under views/scripts/ and use the name of the method as the filename appended with the .phtml extension. So for the settings page my view is located at /User/brad/AtmailPlugins/Atmail/Barracuda4Atmail/views/scripts/settings.phtml

I then load a config file that the plugin uses and then instantiate the ApiCall class, this is just a custom class created to make the actual Barracuda API calls for the plugin:

// Read in the plugin config
$config = new Zend_Config_Ini(dirname(__FILE__) . '/config.ini', 'main');
  
$userData = Zend_Auth::getInstance()->getStorage()->read();
$this->view->account = $userData['Account'];
require_once(dirname(__FILE__) . "/ApiCall.php");
$api = new ApiCall($config, $userData['Account']);

Then I handle fetching all the relevant Barracuda Spam Firewall settings for the user:

$vars = array(
'user_scana_block_level',
'user_scana_tag_level',
'user_scana_quarantine_level',
'user_quarantine_email_address',
'user_spam_scan_enable',
'user_quarantine_enable',
'user_quarantine_email_address',
'user_quarantine_notify',
'user_scana_sender_block',
'user_scana_sender_allow'
);
  
if (!$api->connect()) {
$this->view->error = $api->getLastError();
echo $this->view->render("settings.phtml");
return;
}
  
foreach ($vars as $var) {
  
$res = $api->call("config_get.cgi", array("variable" => $var));
if ($res === false) {
$this->view->error .= $api->getLastError();
break;
} else {
$this->view->$var = $res->$var;
}
}

You will notice I use $this->view->variableName = "someValue", this assigns variables into the view object and they are expanded into their values and inserted into the html output when I call:

echo $this->view->render("settings.phtml");
}

You can see how the variables are used in this sample of the view file:

translate('Quarantine Notification Email Address')?>
  
"user_quarantine_email_address" id="user_quarantine_email_address" value="user_quarantine_email_address ?>" type="text">
translate('Set the email address that will receive quarantine notifications for this account. Default: '); echo $this->account?>

Now that is it for displaying the settings page for the plugin and we move onto implementing the saveSettings() method so we can save the settings the user wants:

public function saveSettings()
{
$this->_initView();
  
// Read in the plugin config
$config = new Zend_Config_Ini(dirname(__FILE__) . '/config.ini', 'main');
  
$userData = Zend_Auth::getInstance()->getStorage()->read();
$this->view->account = $userData['Account'];
require_once(dirname(__FILE__) . "/ApiCall.php");
$api = new ApiCall($config, $userData['Account']);
  
if (!$api->connect()) {
$this->view->error = $api->getLastError();
echo $this->view->render("settings.phtml");
return;
}
  
$vars = array(
'user_scana_block_level',
'user_scana_tag_level',
'user_scana_quarantine_level',
'user_quarantine_email_address',
'user_spam_scan_enable',
'user_quarantine_enable',
'user_quarantine_email_address',
'user_quarantine_notify'
);
  
$lists = array(
'user_scana_sender_block',
'user_scana_sender_allow'
);
  
if (empty($_POST['user_quarantine_email_address'])) {
$_POST['user_quarantine_email_address'] = $this->view->account;
}
  
foreach ($vars as $var) {
  
if (!empty($_POST[$var])) {
  
$res = $api->call("config_set.cgi", array("variable" => $var, "value" => $_POST[$var]));
if ($res === false) {
$this->view->error .= $api->getLastError();
}
}
}
  
foreach ($lists as $list) {
  
// first get all current values for this list
$res = $api->call("config_get.cgi", array("variable" => $list));
  
// delete all current values
$deleteError = false;
foreach ($res->$list as $oldVal) {
$res2 = $api->call("config_delete.cgi", array("variable" => $list, "value" => $oldVal));
if ($res2 === false) {
$this->errors[] = $api->getLastError();
$deleteError = true;
break;
}
}
  
// add new values
if (!$deleteError) {
$newVals = explode("\n", $_POST[$list]);
foreach ($newVals as $newVal) {
$newVal = trim($newVal);
if (!empty($newVal)) {
$res = $api->call("config_add.cgi", array("variable" => $list, "value" => $newVal));
if ($res === false) {
$this->errors[] = $api->getLastError();
}
}   
}
}

Again we initialise the $this->view object by calling $this->_initView(), read in the config, create the $api object and make the required API calls. Then we need to tell the browser what the result of the operation was. As this was an AJAX call (the submit button in settings.phtml submits an AJAX request) we utilise a slightly different method than we did in the settings() method:

require_once("library/jQuery/jQuery.php");
if (count($this->errors)) {
jQuery::addError("Errors occured: " . join("
"
, $this->errors));
} else {
jQuery::addMessage("The settings have been updated");   
}
}
  
echo $this->view->render("jsonresponse.phtml", null, true);
}

In the code above you can see that first we load the library/jQuery/jQuery.php file which is bundled with Atmail and then if we have an error we execute jQuery::addError("Errors occured: " . join("
", $this->errors));
which sets the view to display the error message otherwise if all is good we execute jQuery::addMessage("The settings have been updated"); which tells the view to display a nice green div telling the user all is good.

Last of all we call echo $this->view->render("jsonresponse.phtml", null, true); which sends a JSON response to the browser. The file jsonresponse.phtml is bundled with Atmail and is made available to any views when you set them up with $this->_initView().

Now that's it for the Plugin.php file. The full source code for the plugin can be downloaded from here.

Creating the Plugin Package

Now you'll want to create a plugin package so it can be installed into Atmail. First up make sure any other required directories and resources are added into your plugin directory. For the Barracuda4Atmail plugin under /Users/Brad/AtmailPlugins/Atmail/Barracuda4Atmail/ I have:

Plugin.php
ApiCall.php
config.ini
INSTALL
images/
    spam-settings.png
views/
    scripts/
        settings.phtml

Now I need to package it up:

cd /Users/Brad/AtmailPlugins
tar cvzf Barracuda4Atmail.tgz Atmail/Barracuda4Atmail

This will simply make a gzipped tar file of the Atmail/Barracuda4Atmail directory which is the format Atmail requires for installation via Atmail WebAdmin. That's it, your plugin is ready!

Appendix

ZF Core Plugin Events

  • routeStartup() is called before Zend_Controller_Front calls on the router to evaluate the request against the registered routes.
  • routeShutdown() is called after the router finishes routing the request.
  • dispatchLoopStartup() is called before Zend_Controller_Front enters its dispatch loop.
  • preDispatch() is called before an action is dispatched by the dispatcher. This callback allows for proxy or filter behavior. By altering the request and resetting its dispatched flag (via Zend_Controller_Request_Abstract::setDispatched(false)), the current action may be skipped and/or replaced.
  • postDispatch() is called after an action is dispatched by the dispatcher. This callback allows for proxy or filter behavior. By altering the request and resetting its dispatched flag (via Zend_Controller_Request_Abstract::setDispatched(false)), a new action may be specified for dispatching.
  • dispatchLoopShutdown() is called after Zend_Controller_Front exits its dispatch loop.

Atmail Additional Events

  • filePreview($args=array()) Allows for adding attachment file previews when viewing messages
  • processMessageBody(String $body) Allows for processing/altering message body contents before it is output.
  • emailFrom() Allows for altering From address in outgoing email
  • preLogin() Allows plugins to perform some actions just prior to Atmail trying the IMAP login. This method should return one of three strings:
    "ok" - Causes the core Atmail login code to be skipped, this presumes you've authenticated via some other means.
    "fail" - Some pre login conditions failed and the login will be aborted.
    "" - (Blank string or null) The regular core login code will continue as normal. Userful if you need to perform some pre login tasks but still want Atmail to perform it's regular login.
  • loginFail() Called when a login fails
  • loginSuccess() Called upon all successful logins
  • setup() This method will be called upon plugin installation - if there is any install time setup you need to do, such as create a DB table, then you should implement this method in order to achieve what you need to.

Plugin Helper Methods

The base plugin class Atmail_Controller_Plugin also defines several "helper" methods that perform some common tasks:

  • _initView() From your plugin call $this->_initView() to setup a view object (Zend_View) for your plugin.
  • _createMailSettingsNavSection(Array $params) Creates a nav section in webmail > settings for your plugin.
  • _makeUrl(String $path) Creates a web accessible URL that points to $path ($path should be a relative path to a resource in your plugin directory).

Plugininterface Controller

The plugininterface controller acts as a public interface to all plugins. If your plugin renders a page that needs to submit to or request resources from a plugin (itself or another) then it should do so via the plugininterface controller. This controller takes the request then calls the desired method on the requested plugin. An example URL might be: http://atmail.com/index.php/mail/plugininterface/index/Atmail_Barracuda4Atmail_Plugin/saveSettings which would call the saveSettings() method on the plugin with the class name of Atmail_Barracuda4Atmail_Plugin

For more information on the available plugin events and helper methods see /path/to/atmail/webmail/library/Atmail/Controller/Plugin.php in your Atmail source code.

Have more questions? Submit a request

Comments