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. |
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:
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.
To complete this tutorial, you need to know the following:
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 2git clone https://bitbucket.org/atlassian_tutorial/jira-add-email-handler.git
Alternatively, you can download the source as a ZIP archive.
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.
Set up the Atlassian Plugin SDK and build a project if you did not do that yet.
Open a Terminal and navigate to the directory where you want to keep your app code.
To create an app skeleton, run the following command:
1 2atlas-create-jira-plugin
To identify your app, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Navigate to the project directory created in the previous step.
1 2cd space-blueprint/
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 2rm -rf ./src/test/java rm -rf ./src/test/resources/
Delete the unneeded Java class files.
1 2rm -rf ./src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo/*
Import project in your favorite IDE.
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:
Navigate to the mail-handler-demo
directory created by the SDK.
Open the pom.xml
file.
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>
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.
Update the description
element:
1 2<description>This plugin demonstrates how to add a custom message handler to Atlassian JIRA</description>
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 version | Compatible jira-mail-plugin version |
Jira 7.1 | 10.0.0 |
Jira 7.0 | 8.0.0 |
Jira 6.4 | 7.0.21 |
Jira 6.3 | 6.3.15 |
Save the pom.xml
file.
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.
Navigate to src/main/resources/
and open the atlassian-plugin.xml
file.
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.
Save the 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:
Navigate to src/main/resources
and open the mail-handler-demo.properties
resource file.
Add the following property:
1 2demohandler.name=My Demo Mail Handler
MessageHandler
implementationNow 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.
Navigate to src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo
and create a new file named DemoHandler.java
.
Add the following code to the file:
1 2package 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:
The init()
method is called at message handler set up time, that is, when the administrator configures the message
service and the message handler is instantiated (dependency injection works here).
The message service may:
The params
argument contains the message handler configuration. But you can
choose to keep that data elsewhere, if desired, such as in PropertySet
or by using ActiveObjects
. The module code
may use the monitor
argument to report problems spotted while initializing the handler.
Each time a message is successfully fetched and read, Jira calls the handleMessage()
method. The message
parameter contains the message itself and context
is a thin abstraction that allows you to develop handlers that
work in test mode (where they should not modify Jira) and in normal production mode. More about that later.
Save the file.
Let's start Jira and see what we've got so far.
Open a Terminal and navigate to the app root directory where the pom.xml
is located.
Run this SDK command:
atlas-run
This command downloads and starts Jira with your app installed.
Open the Jira instance in a browser and log in with the default admin/admin.
Create a simple issue tracking project when prompted.
Click , and then click System.
Click Mail > Incoming Mail.
Click Add incoming mail handler. You should see something like this:
That's the mail handler you added.
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 2issueKey TST-1 otherParam abc
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:
atlas-package
command.Our message handler needs one configuration parameter – the issue key defining the issue it will add comments to.
We will validate:
As we are going to use such validation in at least two places, let's encapsulate it as a separate component.
Navigate to src/main/java/com/example/plugins/tutorial/jira/mailhandlerdemo
and create a new file named IssueKeyValidator.java
.
Add the following code:
1 2package 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; } }
Open the DemoHandler.java
file again and replace its content with the following:
1 2package 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.
Save the file.
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.
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.
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.
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 2package 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.
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>
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 2demohandler.issue.key=Issue Key
Reload your app in Jira. As mentioned, you can do this using QuickReload with atlas-package
command, or
simply by restarting Jira.
If you don't already have one, create a Jira issue that your handler can add a comment to. Note its issue key.
Go to the System administration page and try adding the mail handler based on your custom handler.
As before, enter a name for the handler.
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.
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.
Enter the key for an existing issue and click Add.
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 2MIME-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.
The Bitbucket repository for this tutorial includes a text file that contains the sample message.
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.
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: