Last updated Mar 22, 2024

Creating a custom email handler for Jira

Applicable:

Jira 7.0.0 and later.

Level of experience:

Advanced. You should have completed at least one intermediate tutorial before working through this tutorial. See the list of developer tutorials.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

Overview of the tutorial

Administrators can configure Jira to receive and process email messages sent to a particular account on an IMAP or a POP server. Depending on the message handler selected, Jira can create an issue or add a comment to an issue based on message content.

Jira provides several built-in mail handlers, find their description on Creating Issues and Comments from Email page. In addition, app developers can create custom email handlers by implementing a message-handler module.

This tutorial shows you how to build a message handler app. To keep things simple, the app will turn email content into a comment for a specific issue in a project. Also, we'll use Jira's ability to read email files from a local directory rather than configuring an email server. Your completed app will consist of the following components:

  1. Java classes encapsulating the app logic.
  2. Resources for display of the app UI.
  3. An app descriptor (XML file) to enable the plugin module in the Atlassian application.

When you are finished, all these components will be packaged in a single JAR file.

About these instructions

You can use any supported combination of operating system and IDE to construct this app. These instructions were written using macOS Sierra and IntelliJ IDEA 2017.3. If you use another combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with Jira 7.7.1 using Atlassian Plugin SDK 6.3.10.

Before you begin

To complete this tutorial, you need to know the following: 

  1. The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  2. Be familiar with development tools, such as Maven and IDEs.
  3. How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  4. How to configure Jira system settings.

App source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are finished, you can find the app source code on Atlassian Bitbucket.

To clone the repository, run the following command:

1
2
git clone https://bitbucket.org/atlassian_tutorial/jira-add-email-handler.git

Alternatively, you can download the source as a ZIP archive

Step 1. Create the app project

In this step, you'll use an atlas command to generate stub code for your app. The atlas commands are part of the Atlassian Plugin SDK, and automate much of the work of app development for you.

  1. Set up the Atlassian Plugin SDK and build a project if you did not do that yet.

  2. Open a Terminal and navigate to the directory where you want to keep your app code.

  3. To create an app skeleton, run the following command:

    1
    2
    atlas-create-jira-plugin
    
  4. To identify your app, enter the following information.

    group-id

    com.example.plugins.tutorial.jira

    artifact-id

    mail-handler-demo

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.jira.mailhandlerdemo

  5. Navigate to the project directory created in the previous step.

    1
    2
    cd space-blueprint/
    
  6. Delete the test directories.

    Setting up testing for your app isn't part of this tutorial. To delete the generated test skeleton, run the following commands:

    1
    2
    rm -rf ./src/test/java
    rm -rf ./src/test/resources/
    
  7. Delete the unneeded Java class files.

    1
    2
    rm -rf ./src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo/*
    
  8. Import project in your favorite IDE.

Step 2. Modify the POM metadata and add dependencies

It is a good idea to familiarize yourself with the project configuration file, known as the POM (Project Object Model definition file). The POM declares your app's dependencies, build settings, and metadata (information about your app).

Modify the POM as follows:

  1. Navigate to the mail-handler-demo directory created by the SDK.

  2. Open the pom.xml file.

  3. Add your company or organization name and your website URL to the organization element:

    1
    2
    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  4. Update the name element to something more readable:

    1
    2
    <name>Mail Handler Demo</name>
    

    This is the name of your app that will appear on the Manage Add-ons page in the Jira administration console.

  5. Update the description element:

    1
    2
    <description>This plugin demonstrates how to add a custom message handler to Atlassian JIRA</description>
    
  6. Add the dependencies that your app will rely on. Add the following dependencies to the ones added by the SDK:

    1
    2
    ...
    <dependencies>
         ...
         <dependency>
             <groupId>com.atlassian.jira</groupId>
             <artifactId>jira-mail-plugin</artifactId>
             <version>${jira.mail.version}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>com.atlassian.mail</groupId>
             <artifactId>atlassian-mail</artifactId>
             <version>2.8.6</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>javax.mail</groupId>
             <artifactId>mail</artifactId>
             <version>1.4.4</version>
             <scope>provided</scope>
         </dependency>
        ...
    

    Notice that scope value is provided for these dependencies, because the Jira app framework includes those JARs. If you were to use a different scope, it would result in classloader issues related to duplicate classes available on the app classpath. 

    The dependency version for jira-mail-plugin, which is listed earlier as ${jira.mail.version}, should be replaced with the app version suitable for your version of Jira from the following table.

    Jira versionCompatible jira-mail-plugin version
    Jira 7.110.0.0
    Jira 7.08.0.0
    Jira 6.47.0.21
    Jira 6.36.3.15
  7. Save the pom.xml file.

Step 3. Add the message handler module to the app descriptor

For most plugin module types, you can use the Atlassian Plugin SDK to add modules to your app. The module you need for this app is called message-handler and it is one of the exceptions. You'll need to add it manually.

  1. Navigate to src/main/resources/ and open the atlassian-plugin.xml file.

  2. Add the message-handler module as a child of atlassian-plugin.

    1
    2
    <message-handler i18n-name-key="demohandler.name" key="demoHandler"  
            class="com.example.plugins.tutorial.jira.mailhandlerdemo.DemoHandler"
            add-edit-url="/secure/admin/EditHandlerDetailsUsingParams!default.jspa" weight="0"/>
    

    The class attribute identifies our handler implementation class com.example.plugins.tutorial.jira.mailhandlerdemo.DemoHandler. The weight value of 0 means that the handler will be first in the handler selection list in the administration user interface. (Built-in handlers come with a weight of 1 to 5, the lower weight the earlier in the list the handler is displayed.)

    Also notice the add-edit-url value. It defines the resource used to configure our handler. For now, we've set it to a resource that comes with Jira. We'll describe that resource and replace it with our own a little later. 

  3. Save the file.

Step 4. Add UI text to the i18n resource file

When you created the app, the SDK generated an i18n resources file for you. This is where UI text comes from. Add a UI text string to it as follows:

  1. Navigate to src/main/resources and open the mail-handler-demo.properties resource file.

  2. Add the following property:

    1
    2
    demohandler.name=My Demo Mail Handler
    

Step 5. Create the MessageHandler implementation

Now let's create the message handler that was referenced in the app descriptor. We're going to make it simple to start with, and build on this class as we go.

  1. Navigate to src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo and create a new file named DemoHandler.java.

  2. Add the following code to the file:

    1
    2
    package com.example.plugins.tutorial.jira.mailhandlerdemo;
    
    import com.atlassian.jira.service.util.handler.MessageHandler;
    import com.atlassian.jira.service.util.handler.MessageHandlerContext;
    import com.atlassian.jira.service.util.handler.MessageHandlerErrorCollector;
    
    import java.util.Map;
    import javax.mail.Message;
    import javax.mail.MessagingException;
    import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
    
    @Scanned
    public class DemoHandler implements MessageHandler {
        private String issueKey;
        @Override
        public void init(Map<String, String> params, MessageHandlerErrorCollector monitor) {
        }
    
        @Override
        public boolean handleMessage(Message message, MessageHandlerContext context) throws MessagingException {
            return true;
        }
    }
    

    So far, our initial message handler code doesn't do a lot. But it forms a good foundation for building upon, and it gives us a chance to reflect on some concepts. Notice the methods in the class:

  3. Save the file.

Step 6. Build, install, and run the app

Let's start Jira and see what we've got so far.

  1. Open a Terminal and navigate to the app root directory where the pom.xml is located.

  2. Run this SDK command:
    atlas-run 
    This command downloads and starts Jira with your app installed.

  3. Open the Jira instance in a browser and log in with the default admin/admin.

  4. Create a simple issue tracking project when prompted.

  5. Click cog icon, and then click System.

  6. Click Mail > Incoming Mail

  7. Click Add incoming mail handler. You should see something like this:

    That's the mail handler you added.

  8. Enter a name for your handler (it can be anything because we won't save it this time), and then click Next
    Notice the configuration form for this handler. 

    How does Jira know what to display in this step of the wizard?

    It gets it from the add-edit-url parameter in your message-handler declaration. Earlier you set that attribute to /secure/admin/EditHandlerDetailsUsingParams!default.jspa. Jira 5.0 and later provides this built-in resource for the benefit of legacy (Jira 4.x versions) email handlers.

    Legacy handlers take configuration parameters in the list of name-value pairs that are divided by a comma. If you were to enter Handler params text in the field, such as issueKey=TST-1, otherParam=abc, your message handler's init() method would get a params map that consists of:

    1
    2
    issueKey    TST-1
    otherParam  abc
    
  9. Cancel your email handler configuration for now. 

In the next steps we'll update Java class, configuration interface resource, and the add-edit-url target for the post-5.0 world.

From here, you can keep Jira running while you continue development of the app. To reload your app, use QuickReload. It reinstalls your app behind the scenes as you work. To use QuickReload, follow these steps:

  1. Open a new Terminal window and go to the app root folder.
  2. To rebuild your app and trigger QuickReload, run atlas-package command.
  3. When build finishes successfully, Jira reloads the app.
  4. Go back to your browser and test your changes (you may need to refresh the browser page first).

Step 7: Implement a real message handler back end

Our message handler needs one configuration parameter – the issue key defining the issue it will add comments to.

We will validate:

  • The correctness of this parameter.
  • Whether such an issue exists.
  • Whether it's editable.
  • Whether the sender has permissions to comment on it.

As we are going to use such validation in at least two places, let's encapsulate it as a separate component.

  1. Navigate to src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo and create a new file named IssueKeyValidator.java.

  2. Add the following code:

    1
    2
    package com.example.plugins.tutorial.jira.mailhandlerdemo;
    
    import com.atlassian.jira.issue.Issue;
    import com.atlassian.jira.issue.IssueManager;
    import com.atlassian.jira.service.util.handler.MessageHandlerErrorCollector;
    import com.atlassian.plugin.spring.scanner.annotation.component.JiraComponent;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
    import org.apache.commons.lang.StringUtils;
    import javax.inject.Inject;
    
    
    @JiraComponent
    public class IssueKeyValidator {
    
        @ComponentImport
        private final IssueManager issueManager;
    
        @Inject
        public IssueKeyValidator(IssueManager issueManager) {
            this.issueManager = issueManager;
        }
    
    
        public Issue validateIssue(String issueKey, MessageHandlerErrorCollector collector) {
            if (StringUtils.isBlank(issueKey)) {
                collector.error("Issue key cannot be undefined.");
                return null;
            }
    
            final Issue issue = issueManager.getIssueObject(issueKey);
            if (issue == null) {
                collector.error("Cannot add a comment from mail to issue '" + issueKey + "'. The issue does not exist.");
                return null;
            }
            if (!issueManager.isEditable(issue)) {
                collector.error("Cannot add a comment from mail to issue '" + issueKey + "'. The issue is not editable.");
                return null;
            }
            return issue;
        }
    }
    
  3. Open the DemoHandler.java file again and replace its content with the following:

    1
    2
    package com.example.plugins.tutorial.jira.mailhandlerdemo;
    
    import com.atlassian.jira.user.ApplicationUser;
    import com.atlassian.jira.issue.Issue;
    import com.atlassian.jira.service.util.handler.MessageHandler;
    import com.atlassian.jira.service.util.handler.MessageHandlerContext;
    import com.atlassian.jira.service.util.handler.MessageHandlerErrorCollector;
    import com.atlassian.jira.service.util.handler.MessageUserProcessor;
    import com.atlassian.mail.MailUtils;
    import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
    import org.apache.commons.lang.StringUtils;
    
    import java.util.Map;
    import javax.mail.Message;
    import javax.mail.MessagingException;
    
    @Scanned
    public class DemoHandler implements MessageHandler {
        private String issueKey;
        private final IssueKeyValidator issueKeyValidator;
        private final MessageUserProcessor messageUserProcessor;
        public static final String KEY_ISSUE_KEY = "issueKey";
    
        // we can use dependency injection here too!
        public DemoHandler(@ComponentImport MessageUserProcessor messageUserProcessor, IssueKeyValidator issueKeyValidator) {
            this.messageUserProcessor = messageUserProcessor;
            this.issueKeyValidator = issueKeyValidator;
        }
    
        @Override
        public void init(Map<String, String> params, MessageHandlerErrorCollector monitor) {
            // getting here issue key configured by the user
            issueKey = params.get(KEY_ISSUE_KEY);
            if (StringUtils.isBlank(issueKey)) {
                // this message will be either logged or displayed to the user (if the handler is tested from web UI)
                monitor.error("Issue key has not been specified ('" + KEY_ISSUE_KEY + "' parameter). This handler will not work correctly.");
            }
            issueKeyValidator.validateIssue(issueKey, monitor);
        }
    
        @Override
        public boolean handleMessage(Message message, MessageHandlerContext context) throws MessagingException {
            // let's again validate the issue key - meanwhile issue could have been deleted, closed, etc..
            final Issue issue = issueKeyValidator.validateIssue(issueKey, context.getMonitor());
            if (issue == null) {
                return false; // returning false means that we were unable to handle this message. It may be either
                // forwarded to specified address or left in the mail queue (if forwarding not enabled)
            }
            // this is a small util method JIRA API provides for us, let's use it.
            final ApplicationUser sender = messageUserProcessor.getAuthorFromSender(message);
            if (sender == null) {
                context.getMonitor().error("Message sender(s) '" + StringUtils.join(MailUtils.getSenders(message), ",")
                        + "' do not have corresponding users in JIRA. Message will be ignored");
                return false;
            }
            final String body = MailUtils.getBody(message);
            final StringBuilder commentBody = new StringBuilder(message.getSubject());
            if (body != null) {
                commentBody.append("\n").append(StringUtils.abbreviate(body, 100000)); // let trim too long bodies
            }
            // thanks to using passed context we don't need to worry about normal run vs. test run - our call
            // will be dispatched accordingly
            context.createComment(issue, sender, commentBody.toString(), false);
            return true; // returning true means that we have handled the message successfully. It means it will be deleted next.
        }
    }
    

    The init() method makes sure the parameter passed in the handler configuration UI is not empty. The handleMessage() method takes email messages from Jira email service, and then makes an issue comment out of it. For line-by-line details, see the code comments.

  4. Save the file.

Step 8. Improve the configuration UI

Now we have a fully functional email handler, but it still has a rudimentary UI that is prone to errors. Let's fix it and unleash the power of the custom handler configuration UI.

  1. To add a WebWork module to your app, add the following element to the atlassian-plugin.xml file:

    1
    2
     <webwork1 key="actions" name="Actions" class="java.lang.Object">
        <actions>
            <action name="com.example.plugins.tutorial.jira.mailhandlerdemo.EditDemoHandlerDetailsWebAction"
                    alias="EditDemoHandlerDetails"
                    roles-required="admin">
                <view name="input">/view/editDemoHandlerDetails.vm</view>
                <view name="securitybreach">/secure/views/securitybreach.jsp</view>
            </action>
        </actions>
    </webwork1>
    

    This module will render the configuration UI for the handler.

  2. Replace the message-handler module, which you added to the descriptor earlier, with the following:

    1
    2
    <message-handler i18n-name-key="demohandler.name"
         key="demoHandler" class="com.example.plugins.tutorial.jira.mailhandlerdemo.DemoHandler"
         add-edit-url="/secure/admin/EditDemoHandlerDetails!default.jspa"
         weight="0"/>
    

    The message handler will now use WebWork action as the resource for adding or editing handler settings. 

  3. Navigate to src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo, create a new file named EditDemoHandlerDetailsWebAction.java, and then add the following code:

    1
    2
    package com.example.plugins.tutorial.jira.mailhandlerdemo;
    
    import com.atlassian.configurable.ObjectConfigurationException;
    import com.atlassian.jira.plugins.mail.webwork.AbstractEditHandlerDetailsWebAction;
    import com.atlassian.jira.service.JiraServiceContainer;
    import com.atlassian.jira.service.services.file.AbstractMessageHandlingService;
    import com.atlassian.jira.service.util.ServiceUtils;
    import com.atlassian.jira.util.collect.MapBuilder;
    import com.atlassian.plugin.PluginAccessor;
    import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
    
    import java.util.Map;
    
    @Scanned
    public class EditDemoHandlerDetailsWebAction extends AbstractEditHandlerDetailsWebAction {
        private final IssueKeyValidator issueKeyValidator;
    
        public EditDemoHandlerDetailsWebAction(@ComponentImport PluginAccessor pluginAccessor, IssueKeyValidator issueKeyValidator) {
            super(pluginAccessor);
            this.issueKeyValidator = issueKeyValidator;
        }
        private String issueKey;
        public String getIssueKey() {
            return issueKey;
        }
    
        public void setIssueKey(String issueKey) {
            this.issueKey = issueKey;
        }
    
        // this method is called to let us populate our variables (or action state)
        // with current handler settings managed by associated service (file or mail).
        @Override
        protected void copyServiceSettings(JiraServiceContainer jiraServiceContainer) throws ObjectConfigurationException {
            final String params = jiraServiceContainer.getProperty(AbstractMessageHandlingService.KEY_HANDLER_PARAMS);
            final Map<String, String> parameterMap = ServiceUtils.getParameterMap(params);
            issueKey = parameterMap.get(DemoHandler.KEY_ISSUE_KEY);
        }
    
        @Override
        protected Map<String, String> getHandlerParams() {
            return MapBuilder.build(DemoHandler.KEY_ISSUE_KEY, issueKey);
        }
    
        @Override
        protected void doValidation() {
            if (configuration == null) {
                return; // short-circuit in case we lost session, goes directly to doExecute which redirects user
            }
            super.doValidation();
            issueKeyValidator.validateIssue(issueKey, new WebWorkErrorCollector());
        }
    }
    

    The class inherits from AbstractEditHandlerDetailsWebAction, which allows us to concentrate on parameter validation. It takes care of the add, edit, and cancel handler lifecycle itself.

  4. To implement the markup used by the action, navigate to src/main/resources/view and create a new Velocity template file named editDemoHandlerDetails.vm.

    1
    2
    ## couple of available navigation helpers
    #set ($modifierKey = $action.browserUtils.getModifierKey())
    #set ($submitAccessKey = $i18n.getText('AUI.form.submit.button.accesskey'))
    #set ($submitTitle = $i18n.getText('AUI.form.submit.button.tooltip', [$submitAccessKey, $modifierKey]))
    #set ($cancelAccessKey = $i18n.getText('AUI.form.cancel.link.accesskey'))
    #set ($cancelTitle = $i18n.getText('AUI.form.cancel.link.tooltip', [$cancelAccessKey, $modifierKey]))
    <html>
    <head>
        <title>$action.handlerName</title>
    </head>
    <body>
    <form class="aui" action="EditDemoHandlerDetails.jspa" method="POST" name="mailHandlerForm" id="mailHandlerForm">
        <div class="form-body">
            <h2>$action.handlerName</h2>
            <span class="global-errors-location">
                #if ($action.getHasErrorMessages())
                        #foreach ($error in $action.getFlushedErrorMessages())
                    #AUImessage("error" "" $textutils.htmlEncode(${error}) "" "" "" "true")
                #end
                    #end
                </span>
            <input type="hidden" name="atl_token" value="$atl_token">
            <label for="issue-key">$i18n.getText('demohandler.issue.key')</label>
            <input type="text" class="text" id="issue-key" name="issueKey" value="$!textutils.htmlEncode($issueKey)">
    
            <div class="buttons-container form-footer">
                <div class="buttons">
                    #if ($action.editing)
                        #set ($addButtonLabel = $i18n.getText('common.words.save'))
                    #else
                        #set ($addButtonLabel = $i18n.getText('common.forms.add'))
                    #end
                    <input id="addButton" type="submit" class="button" value="$!addButtonLabel" accesskey="$submitAccessKey" title="$submitTitle">
                    <a href="IncomingMailServers.jspa" class="cancel" accesskey="$cancelAccessKey" title="$cancelTitle">$i18n.getText("AUI.form.cancel.link.text")</a>
                </div>
    
            </div>
        </div>
    </form>
    </body>
    </html>
    
  5. You may have noticed the demohandler.issue.key i18n key the Velocity template uses. Add a definition for this property to the mail-handler-demo.properties resources file.

    1
    2
    demohandler.issue.key=Issue Key
    

Step 9. Test the finished app

  1. Reload your app in Jira. As mentioned, you can do this using QuickReload with atlas-package command, or simply by restarting Jira.

  2. If you don't already have one, create a Jira issue that your handler can add a comment to. Note its issue key.

  3. Go to the System administration page and try adding the mail handler based on your custom handler.

    1. As before, enter a name for the handler.

    2. If you like, you can configure an email server to use as the source for the message. Otherwise, keep the default Local Files and click Next.

  4. In the handler configuration screen, enter an issue key value. Notice that if you first enter the key of an issue that doesn't exist, you get an error message, due to our validation code.

  5. Enter the key for an existing issue and click Add.

  6. Now let's create the email message Jira will pick up. Navigate to target/jira/home/import/mail, open the testmessage.txt file and add the following content:

    1
    2
    
    MIME-Version: 1.0
    Received: by 123.45.67.89 with HTTP; Mon, 22 Jul 2013 13:09:38 -0700 (PDT)
    Date: Mon, 22 Jul 2013 13:09:38 -0700
    Delivered-To: admin@admin.com
    Message-ID: <CAKOugVWfh27gSCxgUqJE9QTgOJUJiabS27jw@mail.gmail.com>
    Subject: Test Message Subject
    From: Admin <admin@admin.com>
    To: Admin <admin@admin.com>
    Content-Type: multipart/alternative; boundary=485b397dd4ab88758a04e21f414f
    
    --485b397dd4ab88758a04e21f414f
    Content-Type: text/plain; charset=ISO-8859-1
    
    Test message body.
    
    --485b397dd4ab88758a04e21f414f
    Content-Type: text/html; charset=ISO-8859-1
    
    Test message body.<br>
    
    --485b397dd4ab88758a04e21f414f--
    

    The name of the file is not important. Only that each message occupies its own text file in the directory.

  7. Give Jira a few minutes to find the new message and apply it. When it does, you'll see a log message in the Terminal window where you started Jira:

    1
    2
    [INFO] [talledLocalContainer] 2013-07-23 17:29:26,784 QuartzScheduler_Worker-0 INFO ServiceRunner    MyMailHandler [atlassian.mail.incoming.fileservice] MyMailHandler[/home/atlas/atlassian/final/mail-handler-demo/target/jira/home/import/mail]: Added comment 'Test Message Subj... 'by 'admin' to issue 'TST-1'
    [INFO] [talledLocalContainer] 2013-07-23 17:29:26,784 QuartzScheduler_Worker-0 INFO ServiceRunner    MyMailHandler [atlassian.mail.incoming.fileservice] Deleted file: /home/atlas/atlassian/final/mail-handler-demo/target/jira/home/import/mail/testmessage.txt
    

    It worked! Jira removed the message after applying it, as logged, so you would need to recreate the text file each time you want to test.

  8. Check your issue again. Now you will see a new comment added by your email handler.

Congratulations, that's it!

Have a treat!

Rate this page: