Mail Handler Examples

Find Existing Issue

This simple example reads incoming emails and tries to find an existing issue based on the email subject, and creates a new issue if it does not exist. This example illustrates the use of MessageUserProcessor and MessageHandlerContext.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.service.util.ServiceUtils
import com.atlassian.jira.service.util.handler.MessageUserProcessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.util.UserManager
import com.atlassian.mail.MailUtils

def userManager = ComponentAccessor.getComponent(UserManager)
def projectManager = ComponentAccessor.getProjectManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def messageUserProcessor = ComponentAccessor.getComponent(MessageUserProcessor)

def subject = message.getSubject() as String
def issue = ServiceUtils.findIssueObjectInString(subject) // <1>

if (issue) {
    return // <2>
}

ApplicationUser user = userManager.getUserByName("admin")
ApplicationUser reporter = messageUserProcessor.getAuthorFromSender(message) ?: user // <3>
def project = projectManager.getProjectObjByKey("SRTESTPRJ")

def issueObject = issueFactory.getIssue()
issueObject.setProjectObject(project)
issueObject.setSummary(subject)
issueObject.setDescription(MailUtils.getBody(message))
issueObject.setIssueTypeId(project.issueTypes.find { it.name == "Bug" }.id)
issueObject.setReporter(reporter)

messageHandlerContext.createIssue(user, issueObject) // <4>

Line 14: Uses the ServiceUtils#findIssueObjectInString function to get the issue from the subject.

Line 17: If the issue mentioned in the subject exists, do nothing.

Line 21: Calls MessageUserProcessor#getAuthorFromSender to get the author.

Line 31: Uses the binding variable MessageHandlerContext to create a new issue issue.

Create Issue With Attachments

The following example is similar to the previous example but it shows how to create attachments for an issue from the available attachments in a message.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.util.JiraHome
import com.atlassian.jira.service.services.file.FileService
import com.atlassian.jira.service.util.ServiceUtils
import com.atlassian.jira.service.util.handler.MessageUserProcessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.util.UserManager
import com.atlassian.mail.MailUtils
import org.apache.commons.io.FileUtils

def userManager = ComponentAccessor.getComponent(UserManager)
def projectManager = ComponentAccessor.getProjectManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def messageUserProcessor = ComponentAccessor.getComponent(MessageUserProcessor)
JiraHome jiraHome = ComponentAccessor.getComponent(JiraHome)

def subject = message.getSubject() as String
def issue = ServiceUtils.findIssueObjectInString(subject)

if (issue) {
    return
}

ApplicationUser user = userManager.getUserByName("admin")
ApplicationUser reporter = messageUserProcessor.getAuthorFromSender(message) ?: user
def project = projectManager.getProjectObjByKey("SRTESTPRJ")

def issueObject = issueFactory.getIssue()
issueObject.setProjectObject(project)
issueObject.setSummary(subject)
issueObject.setDescription(MailUtils.getBody(message))
issueObject.setIssueTypeId(project.issueTypes.find { it.name == "Bug" }.id)
issueObject.setReporter(reporter)
issue = messageHandlerContext.createIssue(user, issueObject) // <1>

def attachments = MailUtils.getAttachments(message) // <2>

attachments.each { MailUtils.Attachment attachment ->
    def destination = new File(jiraHome.home, FileService.MAIL_DIR).getCanonicalFile()
    def file = FileUtils.getFile(destination, attachment.filename) as File // <3>
    FileUtils.writeByteArrayToFile(file, attachment.contents)
    messageHandlerContext.createAttachment(file, attachment.filename, attachment.contentType, user, issue) // <4>
}

Line 34: Uses the binding variable MessageHandlerContext to create a new issue issue as the previous example.

Line 36: Retrieve all the attachments form the message using MailUtils#getAttachments method.

Line 40: Create a temporary file in the mail import directory to write attachment content.

Line 42: MessageHandlerContext#createAttachment Creates and adds the attachment to the issue we have created before.

Create Users from CC and Add to Watchers

Let’s say the Jira does not have the user, and the user needs to be created. The following example reads addresses in the CC field of an email and creates users for any contacts not available:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.service.util.ServiceUtils
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.util.UserManager
import com.opensymphony.util.TextUtils

import javax.mail.Address
import javax.mail.Message
import javax.mail.internet.InternetAddress

def userManager = ComponentAccessor.getComponent(UserManager)
def watcherManager = ComponentAccessor.getWatcherManager()

if (userManager.hasWritableDirectory()) { // <1>
    def subject = message.getSubject() as String
    def issue = ServiceUtils.findIssueObjectInString(subject)
    def addresses = message.getRecipients(Message.RecipientType.CC) as List<Address> // <2>

    if (!issue || !addresses) {
        return
    }

    addresses.each {
        InternetAddress internetAddress = it as InternetAddress // <3>
        def email = internetAddress.address
        if (TextUtils.verifyEmail(email)) {
            def fullName = internetAddress.getPersonal() ?: email // <4>
            def user = userManager.getUserByName(fullName)

            if (!user) {
                try {
                    user = messageHandlerContext.createUser(email, null, email, fullName, null) as ApplicationUser // <5>
                    log.debug "User '${user.name}' created"
                } catch (Exception ex) {
                    log.error("Cannot create user", ex)
                }
            }

            watcherManager.startWatching(user, issue) // <6>
        }
    }
}

Line 14: Checks there is at least one directory which allows creating a new user.

Line 17: Retrieves recipients from the message where recipient type is CC. 

Line 24: Casts the address as InternetAddress which helps to get the email address, name etc.

Line 27: In the absence of a name in InternetAddress the email address is considered to be the full name of the user.

Line 32: Uses the messageHandlerContext binding variable to create the user.

Line 39: Uses WatcherManager to add the user to the issue watcher list.

Change Issue Status

The following example shows how ScriptRunner Mail Handler is used to change the status of an issue. This fits a scenario where an email is generated by a service or an automated system where the message body has a structured format. In this example, the email is generated by Bitbucket server to inform the reviewers that a pull request has been merged.

The email contains the name of the branch (containing the issue key) and the pull request status.

The mail handler uses a regular expression to read the issue key and change the issue status to Done.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.workflow.TransitionOptions
import com.atlassian.mail.MailUtils

def issueManager = ComponentAccessor.getIssueManager()
def workflowManager = ComponentAccessor.getWorkflowManager()
def issueService = ComponentAccessor.getIssueService()

def body = MailUtils.getBody(message) // <1>

def statusMatcher = /MERGED/ // <2>
def statusResult = body =~ /$statusMatcher/

def issueMatcher = /SRTESTPRJ-[0-9]*/ // <3>
def issueResult = body =~ /$issueMatcher/

if (statusResult.count && issueResult.count) { // <4>
    def issueKey = issueResult[0] as String

    def issue = issueManager.getIssueObject(issueKey)
    if (!issue) {
        return
    }

    def action = workflowManager.getWorkflow(issue).getActionsByName("Resolve Issue").first() // <5>
    assert action

    def adminUser = ComponentAccessor.userManager.getUserByName("admin")
    def transitionOptions = new TransitionOptions.Builder().skipConditions().skipPermissions().skipValidators().build()

    def validateTransition = issueService.validateTransition(adminUser, issue.id, action.id, issueService.newIssueInputParameters(), transitionOptions) // <6>
    def result = issueService.transition(null, validateTransition)

    if (result.errorCollection.hasAnyErrors()) {
        log.error "Error occurred while transitioning ${issue.key}"
    }
}

Line 9: Reads the mail body from the binding variable message. In this case, the MailUtils class has been used to get the mail body.

Line 11: Uses a regular expression to get the status of the pull request.

Line 14: Uses a regular expression to read the issue key from the branch name.

Line 17: Checks the result status for both of the matches above.

Line 25: Retrieves the desired workflow action, in this case, the Done action.

Line 31: Checks if the transition is valid.