Dwarf Mail Server 1.1.3

Mail Agent Tutorial


Content


Introduction

This tutorial will show you how to write, configure and use a simple mail agent. Mail agents are special services which process mail messages, thus add the application logic layer to the server. The application logic of our sample mail agent is to provide a user interface for executing server console commands. A typical Dwarf-based server contains the Console service for managing the application via special commands issued by the end user. There are many possible ways how to pass a command to the console, including an interactive command prompt built-in to the Console service itself. Another way is provided by the Dwarf HTTP Server, which includes a sample web application with HTML form for executing the console commands. We will show here another way how to do it - by sending a special mail message to the server, containing console commands in its text body. The server will execute the commands and send back a response in a new mail message.


Creating the agent

Each mail agent must extend the base MailAgent class. The extending process consists of two steps:

  1. New agent class must implement one or both of the Preprocessing or Postprocessing interfaces. Preprocessing agents may receive each message just once with the complete set of recipients after the message is received and enqueued for the first time. Postprocessing agents may receive the message each time the particular subset of recipients is finished (i.e. delivered or rejected due to an error).
  2. Agent must also override the preprocessing and/or postprocessing method according to the implemented interfaces, since these methods throw an UnsupportedOperationException by default.

Our agent will be preprocessing, therefore it must implement the corresponding interface and override the required method:

package SK.gnome.dwarf.mail.sample;

import java.io.IOException;
import java.util.Collection;
import SK.gnome.dwarf.mail.MailException;
import SK.gnome.dwarf.mail.smtp.proc.MailAgent;
import SK.gnome.dwarf.mail.smtp.proc.MailAgentMessage;
import SK.gnome.dwarf.mail.smtp.proc.Preprocessing;

public class ConsoleCommand extends MailAgent implements Preprocessing
{
  public ConsoleCommand(String name)
  { super(name);
  }

  protected void preprocess(MailAgentMessage message, Collection recipients)
    throws IOException, MailException
  {
    // add the application logic here
  }
}

From now we will focus only on the preprocess method. It has two arguments - the MailAgentMessage provides an access to the processed message's SMTP envelope information as well as its content, either unparsed or as a MimePart view. The recipients collection contains only those recipients, which have passed through the mail filter chain. Mail filters are subclasses of MailFilter class and are contained by mail agents. They decide which messages and recipients the agent will get on the input. The recipient collection is never empty - that means if all recipients were eventually filtered out, the agent will not get the given message at all. Preprocessing agents may get the complete set of recipients in the input collection, postprocessing only those for which the current delivery process has succeded or permanently failed. Thus a postprocessing agent may receive the same message more than once, each time with a different subset of original recipients as the delivery process proceeds in time and marks the recipients as finished.

If the recipients are to be finished by the agent itself (i.e. they will not enter the delivery phase after the processing by agent), they must be marked explicitly as either delivered, failed or rejected:

    // first of all, mark the passed recipients as finished
    // (if you omit this step, the message will be normally delivered
    // to the given recipients later)

    for (Iterator it = recipients.iterator(); it.hasNext(); )
    { Recipient rcpt = (Recipient)it.next();
      if (!rcpt.isFinished())
        rcpt.finish(Recipient.DELIVERED, "2.1.5", "Delivered to mail agent");
    }

The first argument to finish method specifies the final state of the recipient. It may be either DELIVERED for a successful delivery or processing, or FAILED for an unsuccessful one. The REJECTED state is the same as FAILED except that no DSN notifications are created for the rejected recipients. (Alternatively, you may set the ORCPT attribute of the Recipient object to "NEVER" string, which will actually prevent DSN notification, too.) The second and third arguments specify the extended SMTP status code and the textual reason phrase.

In the case the recipients should be finished as FAILED or REJECTED by interrupting the current message processing operation, you may alternatively throw a FailedDeliveryException from within the method body:

    // interrupt the mail processing and mark the given
    // recipients as finished+failed

    // if (errorCondition)
    //  throw new FailedDeliveryException(Recipient.FAILED, "5.0.0", "Failed due to an error");

In the next part of the preprocess method we get a MimePart view of the message first with the decoded content input stream. The stream is then parsed line by line for the username and password to authenticate the subject, and a set of console commands which are executed and their output is appended to a byte buffer named buf. (See the source code.)

Finally, we create a new message with the commands' output and send it to the original message sender:

    // create and send a response message with the output
    // of executed commands as the message body text

    MimeMessageBuilder msg = new MimeMessageBuilder();
    String postmaster = ((SMTPParameters)getParameters()).getPostmasterAddress();
    msg.setFrom("Mail Delivery Subsystem <" + postmaster + ">");
    msg.setTo(part.getHeader("From", null));
    msg.setSubject("Output of console command");
    msg.setHeader("In-Reply-To", part.getHeader("Message-Id", null));
    ByteArrayInputStream buf = new ByteArrayInputStream(out.toByteArray());
    msg.setContent(buf, "text/plain", "quoted-printable");
    context.sendMessage(msg);

You can see that the MimeMessageBuilder class may be used to create a new MIME messages easily, even a multipart ones. The actual postmaster's address is obtained from the SMTP server configuration via the SMTPParameters object. Message is then sent via sendMessage method of the inherited MailAgentContext object. This object provides some useful methods for processing and sending mail messages, as well as storing them to a local mail folders.
In the case you would need a more control over the message delivery process, you may get a new SMTPMessage instance from the context object to create and send a new mail message. However, it does not provide you any convenience methods for setting message headers or manipulating with the multipart MIME bodies. Instead, you would have to initialize the message data (both header and body) from a raw input stream.

You can see the commented source code of the whole mail agent here. It is member of the SK.gnome.dwarf.mail.sample package and is already compiled into the mail server JAR library coming with the distribution.


Configuring the agent

The mail agent must be added to a MailAgentServer container, along with other mail agents and filters attached to them:

  <service classbase="SK.gnome.dwarf.mail" class=".smtp.SMTPServer" name="SMTP Server">

    <!-- ... -->

    <!-- MAIL AGENTS -->

    <service class=".smtp.proc.MailAgentServer" name="Mail Agent Server">

      <!-- CONSOLE COMMAND AGENT -->

      <service class=".sample.ConsoleCommand" name="Console Command">
        <set name="required">false</set>

        <service class=".smtp.proc.filter.HostIsLocal" name="Host Is Local"/>

        <service class=".smtp.proc.filter.UserIs" name="User Is">
          <set name="checkOriginal">true</set>
          <set name="ignoreCase">true</set>
          <set name="condition">postmaster</set>
        </service>

        <service class=".smtp.proc.filter.SubjectIs" name="Subject Is">
          <set name="ignoreCase">true</set>
          <set name="condition">console command</set>
        </service>

      </service>

    </service>

    <!-- ... -->

  </service>

The inherited required attribute of ConsoleCommand agent specifies whether the message processing will be immediatelly interupted and finished if an error occurs during it. If it is set to false, the error is logged only and the message is passed to the next agent in the chain, otherwise the whole process is interrupted, current recipients are marked as finished and the message is returned to the mail queue.

You can see that three mail filters are needed for our agent. The HostIsLocal and UserIs checks whether the message is for the postmaster local user. The checkOriginal attribute is set to true because the postmaster address is often an alias name, so the recipient collection passed to the filters and agents would contain only those recipients expanded from the alias, not the alias itself. Therefore we need to check the original recipient value first (set by server in the alias-expanding phase) to handle this situation well. The last SubjectIs filter tests whether the message subject equals to the "console command" string, which is actually a trigger for our agent.
These three filters work in a chain, which causes that the console agent will get only messages sent to the local postmaster address with the specific Subject header.

Trying out

The mail agent may be examined directly by running the sample SMTP server application bundled with the distribution package. Create and send an e-mail to "postmaster@your.domain.com", where "your.domain.com" is the host name for which the server is configured to receive mail. The message must have subject equal to "console command" string, otherwise our mail agent won't get it since it would be filtered out by the attached mail filter. The message body may look like this:

joe
asdf
report
cs SMTP Server
report
queue list details

It says that we want to authenticate as user "joe" with password "asdf" and issue the following console commands on behalf of the given user. Send the message and check out the local mailbox for joe either via POP3 or IMAP4 account. You should get back a server reply with the console output included in it.


Return to the main page.


Copyright (c) 2004-2005, Gnome Ltd. All rights reserved.