Wiki

Clone wiki

runner / Custom Mail Handler

Custom Mail Handler

Create your own mail handler, and add all features that you need using your favorite scripting language: php/groovy

Why?

If we can have custom post-functions, conditions, validators, listeners, why not to outsource to GROOVY/PHP the flow of incoming mail processing also? In the JIRA default installation the Mail Plugin provides several mail handlers which allow to create issues and/or comments from email messages, but they are poorly customizable. How about choosing an issue type of the issue being created depending on contents, or setting values of custom fields? Or analyze attachments? These default handlers are quite simple. There's a commercial In-Mail Handler for JIRA plugin, but anyway anything that is not scripted will be always limited.

Using

When you configure Custom Mail Handler, you'll see a few 'standard' options and two already familiar fields: textarea with an inline script and a path to the PHP script file. NOTE: in this case the two scripts are independent, the first one is used to configure the service only, while the second one will be evaluated by the handler to process a message.

Due to the fact that these configuration parameters for a service are stored by the Mail Plugin, there are two restrictions: the values of the fields must not contain commas, and the whole size of the key1=val1,key2=val2... configuration string must be less than 255 characters. I know, ugly.

If the inline script field is empty, it will be filled with a sample configuration. These variables you'll see will be then added to the $params array, which will be passed to the handler init method. Every instance of mail handler will have its own object, so no problem here. Basically: the names of the configuration variables are predetermined, but you can add your own and they will be passed to the handling script. What I recommend is to put the configuration script somewhere in the runner directory using Script Manager, and then just include it from the inline script.

$projectKey="TEST";
$issueType="Bug";
$reporterUserName="admin";
//$reporterUserKey="admin";
$fingerprint="forward"; //forward/accept/ignore
$bulk="ignore"; //ignore/accept/forward/delete
$ccassignee="true";
$ccwatcher="true";
##How to include file? * Groovy

#!java

evaluate(new File("../runner/params.groovy"))
  • PHP
#!php

require()

Why some parameters are 'outside' of inline script? It is important to keep forwardEmail 'outside' of the inline script variables, as the forwarding of unprocessed emails happens outside of handler. Yes, the design of JIRA Mail Plugin leaves much to be desired.

The script must return boolean true to signal that it has processed email and it should be deleted.

The bindings include:

  • $message javax.mail.Message

  • $context com.atlassian.jira.service.util.handler.MessageHandlerContext

  • $monitor com.atlassian.jira.service.util.handler.MessageHandlerExecutionMonitor

  • $handler f.g.jira.plugins.runner.mail.MailHandler

  • $params java.util.Map<String, String>

  • $log org.apache.log4j.Logger

##Examples ###GROOVY * Creation Issue

#!java


import com.atlassian.crowd.embedded.api.User;

$log.warn("Hello from mail handler!");
deleteEmail = false;

if (!$handler.canHandleMessage($message)) {
    $log.debug("Cannot handle message '$subject'.");
    return $deleteEmail;
}

def project = $handler.getProject(); // ComponentAccessor.getProjectManager().getProjectObjByKey(projectKey)  
if (!project) {
    return false;
}
$log.warn("Reporter: '$project'.");

User reporter = $handler.getReporter($message);
$log.warn("Reporter: '$reporter'.");

if (!reporter) {
    return false;
}

if (!$handler.canCreateIssues(reporter, project)) {
    return false;
}

//issueType = $handler.getIssueTypeObjectByName($params["issueType"]); 
def issueType = $handler.getIssueTypeObject(); // ComponentAccessor.getConstantsManager().getIssueTypeObject(issueType)
if (!issueType) {
    return false;
}


def summary = $message.getSubject();
if (!summary) {
    $monitor.markMessageForDeletion("No subject, ignoring");
    return false;
}


def priority = $handler.getPriority($message, project, issueType); // also checks field visibility
def description = $handler.getDescription($message, project, issueType, reporter); // also checks field visibility
def assignee = null;
if ($params["ccassignee"]) {
    assignee = $handler.getFirstValidAssignee($message.getAllRecipients(), project);
}
if (!assignee) {
    assignee = $handler.getDefaultAssignee(project);
}
if (assignee) {
    assigneeId = assignee.getName(); // assignee.name does not work for some reason
}

summary = $handler.truncateSummary(summary);

def issueObject = $handler.getNewIssueObject();

issueObject.setProjectObject(project);
issueObject.setSummary(summary);

if (description) { issueObject.setDescription(description); }

$log.warn("Setting issueType to ${issueType.id}/${issueType.name}");
issueObject.setIssueTypeObject(issueType);
issueObject.setReporter(reporter);
if (assigneeId) { issueObject.setAssigneeId(assigneeId);}
if (priority) { issueObject.setPriorityId(priority); }

// Ensure issue level security is correct
$handler.setDefaultSecurityLevel(issueObject); // $issue.setSecurityLevelId($levelId);

$handler.setCustomFieldDefaults(issueObject);

def attachments = $handler.prepareAttachmentsForMessage($message, issueObject);
$log.warn("I see attachment ${attachments}");
for(AttachmentWrapper att : attachments) {
    $log.warn("I see attachment ${att.md5} ${att.fileName}");
    if (att.getMd5() == "750DBA108138D707044355813A3D2483") {
        att.skip("Just because it sucks");
    }
    if (att.getContentType() == "text/html" && (att.getFileName() == null || att.getFileName() == "null")) {
        $log.warn("Found an text/html attachment");
        att.skip();
        att.setFileName("${att.md5}-orig.html");
    }
}

def issue = $context.createIssue(reporter, issueObject);

$handler.addCcWatchersAndAttachments($message, issue, reporter);

return true;

###PHP

  • Creation Issue
<?php

$log->warn("Hello from mail handler!");
$deleteEmail = false;

if (!$handler->canHandleMessage($message)) {
    $log->debug("Cannot handle message '$subject'.");
    return $deleteEmail;
}

$project = $handler->getProject(); // ComponentAccessor.getProjectManager().getProjectObjByKey(projectKey)  
if (!$project) {
    return false;
}

$reporter = $handler->getReporter($message);
if (!$reporter) {
    return false;
}

if (!$handler->canCreateIssues($reporter, $project)) {
    return false;
}

//$issueType = $handler->getIssueTypeObjectByName($params["issueType"]); 
$issueType = $handler->getIssueTypeObject(); // ComponentAccessor.getConstantsManager().getIssueTypeObject(issueType)
if (!$issueType) {
    return false;
}

$summary = $message->getSubject();
if (!$summary) {
    $monitor->markMessageForDeletion("No subject, ignoring");
    return false;
}
$summary = $handler->truncateSummary($summary);
$priority = $handler->getPriority($message, $project, $issueType); // also checks field visibility
$description = $handler->getDescription($message, $project, $issueType, $reporter); // also checks field visibility

if ($params["ccassignee"]) {
    $assignee = $handler->getFirstValidAssignee($message->getAllRecipients(), $project);
}
if (!$assignee) {
    $assignee = $handler->getDefaultAssignee($project);
}
if ($assignee) {
    $assigneeId = $assignee->getName(); // $assignee->name does not work for some reason
}
//var_dump($assignee);

$issueObject = $handler->getNewIssueObject();
$issueObject->setProjectObject($project);
$issueObject->setSummary($summary);
if ($description) { $issueObject->setDescription($description); }
$log->warn("Setting issueType to ${issueType->id}/${issueType->name}");
$issueObject->setIssueTypeObject($issueType);
$issueObject->setReporter($reporter);
if ($assigneeId) { $issueObject->setAssigneeId($assigneeId);}
if ($priority) { $issueObject->setPriorityId($priority); }

// Ensure issue level security is correct
$handler->setDefaultSecurityLevel($issueObject); // $issue->setSecurityLevelId($levelId);
$handler->setCustomFieldDefaults($issueObject);
$handler->setCustomFieldValue($issueObject,"customfield_10000","value from handler");
$attachments = $handler->prepareAttachmentsForMessage($message, $issueObject);
foreach($attachments as $att) {
    $log->debug("I see attachment ${att->md5} ${att->fileName}");
    if ($att->md5 == "750DBA108138D707044355813A3D2483") {
        $att->skip("Just because it sucks");
    }
    if ($att->contentType == "text/html" && $att->fileName == "") {
        $log->warn("Found an text/html attachment"); // file_get_contents($att->file->canonicalPath)
        $att->skip();
        $att->fileName = rand().".html";
    }
}
$issue = $context->createIssue($reporter, $issueObject);
$handler->addCcWatchersAndAttachments($message, $issue, $reporter);

return true;
?>

Now for creating comments:

<?php

$log->warn("Hello from mail handler!");

$subject = $message->getSubject();

if (!$handler->canHandleMessage($message)) {
    return false;
}

$log->debug("Looking for Issue Key in subject '$subject'.");
$issue = $handler->findIssueObjectInString($subject);
if ($issue == null) {
    $issue = $handler->getAssociatedIssue($message);
}
if ($issue == null) {
    return false;
}

$body = ($params["stripquotes"] == "yes") ? $handler->stripQuotedLines($handler->getEmailBody($message)) : $handler->getEmailBody($message);

$reporter = $handler->getReporter($message);

if ($context->isRealRun() && ! $handler->canAddComments($reporter, $issue)) {
    $errtext = "User $reporter does not have permission to add comments to $issue";
    $log->warn($errtext);
    $context->getMonitor()->markMessageForDeletion($errtext);
    return true;
}

$attachments = $handler->prepareAttachmentsForMessage($message, $issueObject);
foreach($attachments as $att) {
    $log->debug("I see attachment ${att->md5} ${att->fileName}");
    if ($att->md5 == "750DBA108138D707044355813A3D2483") {
        $att->skip("Just because it sucks");
    }
}

if ($context->isRealRun()) {
    $comment = $context->createComment($issue, $reporter, $body, false);    
    $attachmentsChangeItems = $handler->createPreparedAttachments($reporter, $issue);
    $handler->updateHistoryAndFireEvent($attachmentsChangeItems, $issue, $reporter, $comment, false); // false = do not force Issue Commented event
}

return true;
?>

Debugging

Before putting handler on production, first make it solid by creating a test handler bound to a filesystem directory with a test message.

Set the plugin package to DEBUG in the Logging and Profiling Administration page. Also you may set com.atlassian.jira.service.services.mail to DEBUG. The debug messages will appear in the atlassian-jira.log, NOT in the atlassian-jira-incoming-mail.log.

Updated