//package com.onresolve.jira.groovy.canned.workflow.postfunctions
import com.atlassian.core.ofbiz.CoreFactory
import com.atlassian.crowd.embedded.api.Group
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.atlassian.jira.config.properties.ApplicationProperties
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.RendererManager
import com.atlassian.jira.issue.attachment.Attachment
import com.atlassian.jira.issue.customfields.impl.MultiGroupCFType
import com.atlassian.jira.issue.customfields.impl.MultiUserCFType
import com.atlassian.jira.issue.customfields.impl.UserCFType
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.history.ChangeItemBean
import com.atlassian.jira.security.groups.GroupManager
import com.atlassian.jira.security.roles.ProjectRole
import com.atlassian.jira.security.roles.ProjectRoleManager
import com.atlassian.jira.util.AttachmentUtils
import com.atlassian.jira.util.ErrorCollection
import com.atlassian.jira.util.SimpleErrorCollection
import com.atlassian.mail.Email
import com.atlassian.mail.MailException
import com.atlassian.mail.MailFactory
import com.atlassian.mail.queue.SingleMailQueueItem
import com.onresolve.jira.groovy.CannedScriptRunner
import com.onresolve.jira.groovy.canned.CannedScript
import com.onresolve.jira.groovy.canned.utils.ConditionUtils
import com.opensymphony.module.propertyset.PropertySet
import groovy.text.GStringTemplateEngine
import groovy.xml.MarkupBuilder
import org.apache.log4j.Category
import org.ofbiz.core.entity.GenericValue
import javax.activation.DataHandler
import javax.activation.FileDataSource
import javax.mail.BodyPart
import javax.mail.Multipart
import javax.mail.internet.AddressException
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMultipart
import java.sql.Timestamp
import java.util.regex.Matcher
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.issue.search.SearchRequestManager
import com.atlassian.jira.security.JiraAuthenticationContext
import com.atlassian.jira.web.bean.PagerFilter
import java.text.SimpleDateFormat
import com.atlassian.jira.web.bean.BackingI18n
import org.codehaus.groovy.runtime.TimeCategory
import org.apache.commons.lang.StringUtils
import com.atlassian.jira.issue.changehistory.ChangeHistoryManager
class SendCustomEmail implements CannedScript{
public static String FIELD_PREVIEW_ISSUE = "FIELD_PREVIEW_ISSUE"
public static String FIELD_EMAIL_TEMPLATE = "FIELD_EMAIL_TEMPLATE"
public static String FIELD_EMAIL_FORMAT = "FIELD_EMAIL_FORMAT"
public static String FIELD_TO_ADDRESSES = "FIELD_TO_ADDRESSES"
public static String FIELD_TO_USER_FIELDS = "FIELD_TO_USER_FIELDS"
public static String FIELD_CC_ADDRESSES = "FIELD_CC_ADDRESSES"
public static String FIELD_CC_USER_FIELDS = "FIELD_CC_USER_FIELDS"
public static String FIELD_EMAIL_SUBJECT_TEMPLATE = "FIELD_EMAIL_SUBJECT_TEMPLATE"
public static String FIELD_INCLUDE_ATTACHMENTS = "FIELD_INCLUDE_ATTACHMENTS"
public static String FIELD_FROM = "FIELD_FROM"
public static String FIELD_INCLUDE_ATTACHMENTS_NONE = "FIELD_INCLUDE_ATTACHMENTS_NONE"
public static String FIELD_INCLUDE_ATTACHMENTS_NEW = "FIELD_INCLUDE_ATTACHMENTS_NEW"
public static String FIELD_INCLUDE_ATTACHMENTS_ALL = "FIELD_INCLUDE_ATTACHMENTS_ALL"
public static String FIELD_INCLUDE_ATTACHMENTS_CUSTOM = "FIELD_INCLUDE_ATTACHMENTS_CUSTOM"
public static String FIELD_INCLUDE_ATTACHMENTS_CALLBACK = "FIELD_INCLUDE_ATTACHMENTS_CALLBACK"
ComponentManager componentManager = ComponentManager.getInstance()
def issueManager = ComponentAccessor.getIssueManager()
def watcherManager = ComponentAccessor.getWatcherManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager.class)
def groupManager = ComponentAccessor.getComponent(GroupManager.class)
def userUtil = ComponentAccessor.getUserUtil()
def mailServerManager = ComponentAccessor.getMailServerManager()
def mailServer = mailServerManager.getDefaultSMTPMailServer()
Category
log = Category.
getInstance(SendCustomEmail.
class)
String getName() {
"Send a custom email"
}
public String getHelpUrl() {
"https://j...content-available-to-author-only...n.net/wiki/display/GRV/Built-In+Scripts#Built-InScripts-Sendacustomemail"
}
String getDescription() {
"Send an email based on the provided template if conditions are met"
}
List getCategories() {
["Function","ADMIN", "Listener"]
}
List getParameters(Map<String,String> params) {
def toFieldDesc = """Who to send the email to - valid issue fields such as assignee, reporter, projectLead,
componentLead, watchers, user or user group custom fields,<br> or custom fields holding valid
email address, eg <i>customfield_12345</i>,<br>or role:<i>Rolename</i>, e.g role:Developers,
or group:<i>Groupname</i>. Use space to delimit."""
def outputs = [
ConditionUtils.getConditionParameter(),
[
Name:FIELD_EMAIL_TEMPLATE,
Label:"Email template",
Description:"""Write a template. Can be plain text or use the
<a href=\"http://d...content-available-to-author-only...s.org/display/GROOVY/Groovy+Templates#GroovyTemplates-GStringTemplateEngine\">GStringTemplateEngine</a>.
See wiki for examples.""",
Type:"text",
],
[
Name:FIELD_EMAIL_SUBJECT_TEMPLATE,
Label:"Subject template",
Description:"""Subject template. Can be plain text or use the
<a href=\"http://d...content-available-to-author-only...s.org/display/GROOVY/Groovy+Templates#GroovyTemplates-GStringTemplateEngine\">GStringTemplateEngine</a>.
See wiki for examples or click below.""",
Examples: [
"""Issue XYZ-1 requires your approval""" : "Issue \$issue requires your approval"
]
],
[
Name:FIELD_EMAIL_FORMAT,
Label:"Email format",
Description:"Whether to send as plain text or HTML.",
Type:"list",
Values: [TEXT:'Plain text', HTML:'HTML'],
],
[
Name:FIELD_TO_ADDRESSES,
Label:"To addresses",
Description:" Who to send the email to. Use commas/space to delimit.",
],
[
Name:FIELD_TO_USER_FIELDS,
Label:"To issue fields",
Description: toFieldDesc,
],
[
Name:FIELD_CC_ADDRESSES,
Label:"CC addresses",
Description:" Who to include in CC. Use commas/space to delimit.",
],
[
Name:FIELD_CC_USER_FIELDS,
Label:"CC issue fields",
Description: toFieldDesc.replaceAll(" send ", " CC "),
],
[
Name:FIELD_INCLUDE_ATTACHMENTS,
Label:"Include attachments",
Type:"radio",
Values: [
(FIELD_INCLUDE_ATTACHMENTS_NONE): "None",
(FIELD_INCLUDE_ATTACHMENTS_NEW): "New",
(FIELD_INCLUDE_ATTACHMENTS_ALL): "All",
(FIELD_INCLUDE_ATTACHMENTS_CUSTOM): "Custom",
],
Description: """Include the issue attachments in the email. You can specify none, or only attachments
that were added in the transition pertaining to this event, or all attachments."""
],
[
Name: FIELD_INCLUDE_ATTACHMENTS_CALLBACK,
Label: "Custom attachment callback",
Description: "Enter a closure which will be called with each Attachment object. Only relevant if you " +
"choose 'Custom' above.",
Type: "text",
Examples: [
"Include only PDFs" : '{a -> a.filename.toLowerCase().endsWith(".pdf")}',
"Include only PDFs added this transition" : '{a -> a.isNew() && a.filename.toLowerCase().endsWith(".pdf")}',
"Only files less than 1 Mb" : '{a -> a.filesize < 1024**2}',
]
],
[
Name:FIELD_FROM,
Label:"From email address",
Description:""" What email address to send the mail from, eg jamie@example.com.
Leave blank for default (<i>${mailServer?.getDefaultFrom()}</i>).""",
],
[
Name:FIELD_PREVIEW_ISSUE,
Label:"Preview Issue Key",
Description:"""Issue key for previewing what the mail will look like.
ONLY used when previewing from the Admin section""",
],
]
outputs
}
ErrorCollection doValidate(Map params, boolean forPreview) {
ErrorCollection errorCollection = new SimpleErrorCollection()
String prvwIssueKey = params[FIELD_PREVIEW_ISSUE] as String
if (forPreview) {
if (! issueManager.getIssueObject(prvwIssueKey as String))
errorCollection.addError(FIELD_PREVIEW_ISSUE, "This issue doesn't exist.")
String cond = params[ConditionUtils.FIELD_CONDITION] as String
if (cond) {
try {
MutableIssue issue = issueManager.getIssueObject(prvwIssueKey)
ConditionUtils.processCondition(cond, issue, true,
[event: new IssueEvent(issue, [:] , null, 1)])
/*
{
def getChangeLog() {
[getRelated : {[]}]
}
})
*/
}
catch (Exception e) {
errorCollection.addError(ConditionUtils.FIELD_CONDITION, e.getMessage())
log.
debug(e.
getMessage()) }
}
}
if (! params[FIELD_EMAIL_TEMPLATE]) {
errorCollection.addError(FIELD_EMAIL_TEMPLATE, "You must provide a template.")
}
if (! params[FIELD_EMAIL_SUBJECT_TEMPLATE]) {
errorCollection.addError(FIELD_EMAIL_SUBJECT_TEMPLATE, "You must provide a subject.")
}
if (! (params[FIELD_TO_ADDRESSES] || params[FIELD_TO_USER_FIELDS])) {
errorCollection.addErrorMessage ("You must provide either fields or addresses to send emails to.")
}
// todo: validation of "field" addresses, to and cc
[FIELD_TO_ADDRESSES, FIELD_CC_ADDRESSES].each {fieldParam ->
if (params[fieldParam]) {
def List invalid = getTextAddresses(params[fieldParam] as String).asList().findAll {String a ->
! validateEmail(a)
}
if (invalid)
errorCollection.addError(fieldParam, "These don't look like valid email addresses: " + invalid.join(", "))
}
}
if (params[FIELD_FROM]) {
if (! validateEmail(params[FIELD_FROM] as String)) {
errorCollection.addError(FIELD_FROM, "Email is not valid: ${params[FIELD_FROM]}")
}
}
if ((params[FIELD_INCLUDE_ATTACHMENTS] == FIELD_INCLUDE_ATTACHMENTS_CUSTOM) && ! params[FIELD_INCLUDE_ATTACHMENTS_CALLBACK]) {
errorCollection.addError(FIELD_INCLUDE_ATTACHMENTS_CALLBACK, "You must provide a callback if you select Custom.")
}
errorCollection
}
private boolean validateEmail (String email) {
// don't use EmailValidator.getInstance().isValid, not worth the trouble of importing the classes
try {
new InternetAddress(email).validate()
}
catch (AddressException ae) {
return false
}
true
}
public Map sendMail (Map params) {
MutableIssue issue = params['issue'] as MutableIssue
// preview mode
if (!issue) {
issue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
}
Boolean doIt = ConditionUtils.processCondition(params[ConditionUtils.FIELD_CONDITION] as String, issue, false, params)
if (! doIt) {
return [:]
}
// do validation again
String emailFormat = params[FIELD_EMAIL_FORMAT]
log.
debug("emailFormat: $emailFormat")
if (mailServer && ! MailFactory.isSendingDisabled()) {
log.
debug("Preparing to send the mail"); def template = mergeEmailTemplateBody(params)
def body = template.toString()
// no space when using separator: http://stackoverflow.com/a/12120203/1018918
String toRecipientList = getAllToAddresses(issue, params).join(',');
log.
debug("To Recipientlist = ${toRecipientList}");
def ccRecipientList = getAllCCAddresses(issue, params).join(',');
log.
debug("CC Recipientlist = ${ccRecipientList}");
def email = new Email(toRecipientList, ccRecipientList, "");
// Now the message body.
addAttachmentsToMail(params, issue, email)
email.setFrom(params[FIELD_FROM] as String ?: mailServer.getDefaultFrom())
email.setSubject(mergeEmailTemplateSubject(params).toString())
email.setMimeType(emailFormat == "HTML" ? "text/html" : "text/plain")
email.setBody(body)
try {
log.
debug ("Sending mail to ${email.getTo()}") log.
debug ("with body ${email.getBody()}") log.
debug ("from template ${params[FIELD_EMAIL_TEMPLATE]}") SingleMailQueueItem item = new SingleMailQueueItem(email);
ComponentAccessor.getMailQueue().addItem(item);
}
catch (MailException e) {
log.
warn ("Error sending email", e
) }
}
else {
log.
warn ("No mail server or sending disabled.") }
return params
}
Map doScript(Map params) {
Thread executorThread = new Thread(new Runnable() {
void run() {
Thread.sleep(500)
sendMail(params)
}
})
// run in separate thread so mail handler has a chance to assemble all attachments
// https://j...content-available-to-author-only...n.net/browse/GRV-284
executorThread.start()
[:]
}
private def addAttachmentsToMail(Map<String,String> params, MutableIssue issue, Email email) {
if (params[FIELD_INCLUDE_ATTACHMENTS] && params[FIELD_INCLUDE_ATTACHMENTS] != FIELD_INCLUDE_ATTACHMENTS_NONE) {
Multipart mp = new MimeMultipart("mixed");
List<Long> attachmentIds = issue.getAttachments()*.id
List<Long> newAttachmentIds = []
if (params[FIELD_INCLUDE_ATTACHMENTS] == FIELD_INCLUDE_ATTACHMENTS_NEW) {
if (!(params["event"] || params["transientVars"])) {
params.put("event", fakeLatestEvent(issue))
log.
debug("No event or transient vars - must be admin screen mode - creating latest event: " + params.
get("event")) }
}
if (params["event"]) { // listener
List changeItems = params["event"].getChangeLog()?.getRelated("ChildChangeItem")
log.
debug("changeItems: $changeItems") changeItems.each {GenericValue gv ->
if (gv["field"] == "Attachment" && gv["newvalue"]) {
newAttachmentIds.add(gv["newvalue"] as Long)
}
}
}
else if (params["transientVars"]) {
List changeItems = params["transientVars"]["changeItems"] as List
changeItems.each {ChangeItemBean cib ->
if (cib.getField() == "Attachment" && cib.getTo()) {
newAttachmentIds.add(cib.getTo() as Long)
}
}
}
attachmentIds.each {attachmentId ->
if (params[FIELD_INCLUDE_ATTACHMENTS] == FIELD_INCLUDE_ATTACHMENTS_NEW) {
if (! newAttachmentIds.contains(attachmentId)) {
return
}
}
def attachment = issue.attachments.find {attachment ->
attachment.id == attachmentId
}
if (params[FIELD_INCLUDE_ATTACHMENTS] == FIELD_INCLUDE_ATTACHMENTS_CUSTOM) {
def gse = CannedScriptRunner.getGse()
try {
def callback = gse.eval(params[FIELD_INCLUDE_ATTACHMENTS_CALLBACK])
// todo: isNew method - see previous versions for ideas
def mailAttachment = new MailAttachment(attachment)
mailAttachment.setIsNew(newAttachmentIds.contains(attachment.id))
def result = callback.call(mailAttachment)
// Attachment.metaClass.
log.
debug "callback result for ${attachment.filename}: " + (result as Boolean
) if (! result) {
return
}
} catch (e) {
log.
error("Failed to evaluate closure for adding attachment to email", e
) return
}
}
File attFile = AttachmentUtils.getAttachmentFile(attachment)
BodyPart attPart = new MimeBodyPart()
FileDataSource attFds = new FileDataSource(attFile)
attPart.setDataHandler(new DataHandler(attFds))
attPart.setFileName(attachment.filename)
log.
debug("Attaching ${attachment.filename} to mail") mp.addBodyPart(attPart)
}
email.setMultipart(mp)
}
}
static Set getTextAddresses(String toConfig) {
Set addresses = new HashSet()
if (! toConfig) {
return addresses
}
if (toConfig) {
for (String f in toConfig.split(/[\s,;]+/)) {
addresses.add(f)
}
}
addresses
}
List getAllToAddresses(Issue issue, Map params) {
String toUserFields = params[FIELD_TO_USER_FIELDS]
Set addresses = getAddressesFromFields(issue, toUserFields)
addresses.addAll(getTextAddresses(params[FIELD_TO_ADDRESSES] as String))
addresses.toList()
}
List getAllCCAddresses(Issue issue, Map params) {
String ccUserFields = params[FIELD_CC_USER_FIELDS]
Set addresses = getAddressesFromFields(issue, ccUserFields)
addresses.addAll(getTextAddresses(params[FIELD_CC_ADDRESSES] as String))
addresses.toList()
}
Set getAddressesFromFields(Issue issue, String toConfig) {
Set addresses = new HashSet()
if (! toConfig) {
return addresses
}
// for testing: "reporter,assignee, watchers, customfield_10020, customfield_10040, customfield_10041, customfield_10042, customfield_10043, group:"a b""
String patStr = /([^\s]*"[^"]*")|([^\s"']+)/
Matcher matcher = toConfig =~ patStr
log.debug("toConfig: \"$toConfig\"")
if (matcher.getCount() == 0) {
return []
}
for (int i in 0..matcher.getCount()-1) {
String f = matcher[i][0]
if (f) {
f = f.trim()
log.debug ("field f: \"$f\"")
if(['reporter', 'assignee'].contains(f)) {
addresses.add((issue.getAt(f) as User)?.emailAddress)
}
else if (f == "watchers") {
watcherManager.getCurrentWatcherUsernames(issue.genericValue).each {String username ->
addresses.add(userUtil.getUser(username).emailAddress)
}
// doesn't work in jira 4.x
// addresses.addAll(watcherManager.getCurrentWatchList(issue.genericValue).collect {it.email})
}
else if (f.toLowerCase() == "projectlead") {
addresses.add(issue.projectObject?.lead?.emailAddress)
}
else if (f.toLowerCase() == "componentlead") {
issue.componentObjects*.lead.each {
addresses.add(userUtil.getUser(it)?.emailAddress)
}
}
else if (f.toLowerCase().startsWith("customfield_")) {
CustomField cf = customFieldManager.getCustomFieldObjects(issue).find {it.id == f} as CustomField
if (cf.getCustomFieldType() instanceof UserCFType) {
addresses.add(cf.getValue(issue)?.emailAddress)
}
else if (cf.getCustomFieldType() instanceof MultiUserCFType) {
addresses.addAll(cf.getValue(issue)?.collect{it.emailAddress})
}
else if (cf.getCustomFieldType() instanceof MultiGroupCFType) {
addresses.addAll (cf.getValue(issue).collect {Group group ->
groupManager.getUsersInGroup(group).collect {userUtil.getUser(it as String)?.emailAddress}
}.flatten())
}
else if (isTextField(cf)) {
// todo: need null check here
addresses.addAll((cf.getValue(issue) as String).split(/[\s,;]+/))
}
else {
log.
debug("Unhandled custom field type $f, but will have a go anyway") addresses.addAll((cf.getValue(issue) as String).split(/[\s,;]+/))
}
}
else if (f.toLowerCase().startsWith("role:")) {
f = f.replaceFirst("role:", "")
f = f.replaceAll("\"", "")
ProjectRole role = projectRoleManager.getProjectRole(f)
if (role) {
addresses.addAll (projectRoleManager.getProjectRoleActors(role, issue.projectObject).getUsers()*.emailAddress)
}
else {
log.
warn ("Could not find role named \"$f\"") }
}
else if (f.toLowerCase().startsWith("group:")) {
f = f.replaceFirst("group:", "")
f = f.replaceAll("\"", "")
Group group = userUtil.getGroup(f)
if (group) {
addresses.addAll(groupManager.getUsersInGroup(group)*.emailAddress)
log.
debug "addresses: $addresses" }
else {
log.
warn ("Could not find group named \"$f\"") }
}
else {
log.
warn ("Could not handle field $f") }
}
}
addresses.minus([null])
}
// https://s...content-available-to-author-only...n.com/browse/GRV-102
Boolean isTextField (CustomField cf) {
return ["com.atlassian.jira.issue.customfields.impl.TextCFType",
"com.atlassian.jira.issue.customfields.impl.GenericTextCFType"].any {
try {
return Class.forName(it).isAssignableFrom(cf.getCustomFieldType().class)
}
catch (Exception e) {
return false
}
}
}
String getDescription(Map params, boolean forPreview) {
if (!forPreview) {
return getDescription()
}
// remove the event otherwise it goes in to the page as a String
Writable template = mergeEmailTemplateBody(params, true)
String emailFormat = params[FIELD_EMAIL_FORMAT]
MutableIssue prvwIssue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
// add the changeitems for the most recent event so listeners can be better tested
params.put("event", fakeLatestEvent(prvwIssue))
def sendTo = getAllToAddresses(prvwIssue, params)
def sendCc = getAllCCAddresses(prvwIssue, params)
// todo: should do all work on this email object, and display attributes in the table
def email = new Email("dummy@example.com")
addAttachmentsToMail(params, prvwIssue, email)
def attachmentsDisplay = ""
if (email.getMultipart()?.getCount()) {
attachmentsDisplay = (0 .. email.getMultipart().getCount() - 1).collect {kount ->
email.getMultipart().getBodyPart(kount).getFileName()
}.join(", ")
}
Boolean condition = ConditionUtils.processCondition(params[ConditionUtils.FIELD_CONDITION] as String, prvwIssue, false)
StringWriter writer = new StringWriter()
MarkupBuilder builder = new MarkupBuilder(writer)
builder.table {
tr{
td{b("Condition")}
td("The condition evaluated to $condition (note that for listeners the condition can't normally be tested)")
}
tr{
td{b("To")}
td(style:'background-color:#ffffff', sendTo.join(", "))
}
tr{
td{b("Cc")}
td(style:'background-color:#ffffff', sendCc.join(", "))
}
tr{
td{b("From")}
td(style:'background-color:#ffffff', params[FIELD_FROM] as String ?: mailServer.getDefaultFrom())
}
tr{
td{b("Subject")}
td(style:'background-color:#ffffff', mergeEmailTemplateSubject(params, true).toString())
}
tr{
td{b("Attachments")}
td(style:'background-color:#ffffff', attachmentsDisplay)
}
tr{
td(valign:'top') {
b("Body")
}
td(style:'background-color:#ffffff'){
if (emailFormat == 'TEXT') {
pre(template.toString())
}
else {
p(mkp.yieldUnescaped (template.toString()))
}
}
}
}
writer.toString()
}
private IssueEvent fakeLatestEvent(MutableIssue prvwIssue) {
Collection<GenericValue> changeGroups = CoreFactory.getGenericDelegator().findByAnd("ChangeGroup", ["issue": prvwIssue.getId()]);
IssueEvent event
if (changeGroups) {
def changeGroup = changeGroups ? changeGroups.last() : null
event = new IssueEvent(prvwIssue, null, null, null, changeGroup, [:], 1, false)
}
else {
event = new IssueEvent(prvwIssue, [:] , null, 1)
}
return event
}
public Writable mergeEmailTemplateBody(Map params, Boolean isPreview = false) {
mergeEmailTemplate(params, params[FIELD_EMAIL_TEMPLATE] as String, isPreview)
}
public Writable mergeEmailTemplateSubject(Map params, Boolean isPreview = false) {
mergeEmailTemplate(params, params[FIELD_EMAIL_SUBJECT_TEMPLATE] as String, isPreview)
}
private Writable mergeEmailTemplate(Map params, String template, Boolean isPreview = false) {
GStringTemplateEngine engine = new GStringTemplateEngine()
Map binding = [:]
binding.putAll(params)
MutableIssue issue
if (params["issue"]) {
issue = params["issue"] as MutableIssue
}
else {
issue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
}
// todo document new things in the binding
ApplicationProperties applicationProperties = componentManager.getApplicationProperties()
binding.put("baseUrl", applicationProperties.getString(APKeys.JIRA_BASEURL))
binding.put("baseurl", applicationProperties.getString(APKeys.JIRA_BASEURL))
binding.put("componentManager", componentManager)
binding.put("issue", issue)
// add last comment to binding - render according to render method chosen
def commentManager = ComponentAccessor.getCommentManager()
def comments = commentManager.getComments(issue)
def lastComment = null
if (comments) {
lastComment = comments.last().body
if (params[FIELD_EMAIL_FORMAT] == "HTML") {
def rendererManager = ComponentAccessor.getComponent(RendererManager.class)
def fieldLayoutItem = ComponentAccessor.getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem("comment")
def renderer = rendererManager.getRendererForField(fieldLayoutItem)
lastComment = renderer.render(lastComment, null)
}
}
binding.put("lastComment", lastComment)
binding.putAll(ConditionUtils.setupBinding(issue, binding))
engine.createTemplate(template).make(binding)
}
public Boolean isFinalParamsPage(Map params) {
true
}
}
public class MailAttachment {
private Attachment delegate
private boolean isNew
boolean isNew() {
return isNew
}
void setIsNew(boolean aNew) {
isNew = aNew
}
MailAttachment(Attachment delegate) {
this.delegate = delegate
}
PropertySet getProperties() {
return delegate.getProperties()
}
Issue getIssueObject() {
return delegate.getIssueObject()
}
GenericValue getIssue() {
return delegate.getIssue()
}
Long getId() {
return delegate.getId()
}
String getMimetype() {
return delegate.getMimetype()
}
String getFilename() {
return delegate.getFilename()
}
Timestamp getCreated() {
return delegate.getCreated()
}
Long getFilesize() {
return delegate.getFilesize()
}
String getAuthor() {
return delegate.getAuthor()
}
}
//SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd")
template = '''
<!-- ################################################################### -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
table {
border-collapse: collapse;
font-size: 10px;
}
td, th {
border: 1px solid #000;
padding:4px;
}
.centerCol {
text-align: center;
}
.highlightRed {
background-color: #FF3333;
}
.highlightYellow {
background-color: #FFFF66;
}
.highlightGreen {
background-color: #66FF99;
}
.highlightGrey {
background-color: lightgrey;
}
</style>
</head>
<body>
<b>List of SLA relevant Incidents</b><br/>
<br/>
<table>
<tr class=highlightGrey>
<th>Prio</th>
<th>Key</th>
<th>Summary</th>
<th>Status</th>
<th>Created date</th>
<th>1st Resolved</th>
<th>1st INT</th>
<th>1st PROD</th>
<th>SLA due date</th>
<th>Remaining</th>
<th>Asignee</th>
<th>RE [MD]</th>
</tr>
<%
issues.each { issue ->
def cssClass = 'highlightGreen'
cssClass = ( issue.escalation == 2 ? 'highlightYellow' : cssClass)
cssClass = ( issue.escalation == 3 ? 'highlightRed' : cssClass)
out << "<tr class='" << cssClass << "'>"
out << "<td class='centerCol'>" << issue.priority_id << "</td>"
out << "<td><a href='https://w...content-available-to-author-only...d.de/browse/" << issue.key << "'>" << issue.key << "</a></td>"
out << "<td>" << issue.summary << "</td>"
out << "<td>" << issue.status << "</td>"
out << "<td>" << issue.created << "</td>"
out << "<td>" << issue.resolved_date << "</td>"
out << "<td>" << issue.integration_date << "</td>"
out << "<td>" << issue.production_date << "</td>"
out << "<td>" << issue.due << "</td>"
out << "<td>" << issue.remaining << "</td>"
out << "<td>" << issue.assignee << "</td>"
out << "<td class='centerCol'>" << issue.estimate << "</td>"
out << "</tr>\\n"
}
%>
</table><br/>
<br/>
Visit Jira to see all incidents <a href='https://w...content-available-to-author-only...d.de/issues/?filter=12106'>link</a><br/>
<br/>
<b>SLA due date (Open -> 1st Resolved)</b>
<br/>
<table width="250" >
<tr class=centerCol>
<td class=highlightGrey> Prio 1 </td>
<td > 3 days </td>
</tr>
<tr class=centerCol>
<td class=highlightGrey> Prio 2 </td>
<td > 10 days </td>
</tr>
</table>
<br/>
<b>Escalation</b>
<br/>
<table width="250" >
<tr class=centerCol>
<td class=highlightGrey> remaining > 1 day </td>
<td class=highlightGreen> ok </td>
</tr>
<tr class=centerCol>
<td class=highlightGrey>
remaining <= 1 day<br/>
remaining > - 5 days</td>
<td class=highlightYellow> warrning </td>
</tr>
<tr class=centerCol>
<td class=highlightGrey> remaining <= - 5 days </td>
<td class=highlightRed> escalation </td>
</tr>
</table>
</body>
</html>
<!-- ################################################################### -->
'''
Long filterId = 12106
def secInHour = 28800
//def dFormat = 'yyyy-MM-dd HH:mm:ss'
def dFormat = 'yyyy-MM-dd HH:mm'
def maxLen = 50
def issues = []
toResolved = 'Resolved'
toIntegration = 'Integration'
toProduction = 'Production'
toDocumentation = 'Documentation'
toClosed = 'Closed'
Date resolved = new Date(0)
String resolved_date = ""
String integration_date = ""
String production_date = ""
String closed_date = ""
def myIssueManager = ComponentAccessor.getIssueManager()
//BackingI18n i18n = ComponentAccessor.getJiraAuthenticationContext().getI18nHelper()
MutableIssue myIssue = myIssueManager.getIssueObject('KTINC-2086')
SearchService searchService = ComponentAccessor.getComponent(SearchService.class);
JiraAuthenticationContext jiraAuthenticationContext = ComponentAccessor.getJiraAuthenticationContext()
user = jiraAuthenticationContext.getLoggedInUser()
//jqlQuery = 'project = "KT INC" AND priority in ("Showstopper (Prio1)", "Severe (Prio2) ") AND status not in (Closed) ORDER BY priority, createdDate'
SearchRequestManager searchRequestManager = componentManager.getSearchRequestManager()
SearchRequest filter = searchRequestManager.getSharedEntity(filterId)
jqlQuery = filter.query.getQueryString()
//jqlQuery = 'project = "KT-Incident Management DC KSC/TSC" AND priority in ("Showstopper (Prio1)", "Severe (Prio2) ") AND status not in (Closed, Documentation) ORDER BY priority, createdDate'
SearchService.ParseResult parseResult = searchService.parseQuery(user, jqlQuery);
ChangeHistoryManager changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
if (parseResult.isValid()) {
def searchResult = searchService.search(user, parseResult.getQuery(), PagerFilter.getUnlimitedFilter())
//issues = searchResult.issues
use (TimeCategory) {
searchResult.issues.each { el ->
Map issue = [:]
changeItems = changeHistoryManager.getAllChangeItems(el)
resolved_date = ""
integration_date = ""
production_date = ""
closed_date = ""
changeItems.findAll{it.getField() == 'status'}.each{
// get only first transition to given status
if (it.getTo() == toResolved && it.getTo() != it.getFrom() && resolved_date == "") {
resolved = it.created
resolved_date = it.created.format(dFormat)
}
if (it.getTo() == toIntegration && it.getTo() != it.getFrom() && integration_date == "") {
integration_date = it.created.format(dFormat)
}
if (it.getTo() == toProduction && it.getTo() != it.getFrom() && production_date == "") {
production_date = it.created.format(dFormat)
}
if (it.getTo() == toClosed && it.getTo() != it.getFrom() && closed_date == "") {
closed_date = it.created.format(dFormat)
}
}
// if given status was skiped then use closed date
if (resolved_date == "" && closed_date != "") {
resolved_date = closed_date
}
if (integration_date == "" && closed_date != "") {
integration_date = closed_date
}
if (toProduction == "" && closed_date != "") {
production_date = closed_date
}
def due = (el.priority.id == '1' ? (el.getCreated() + 3.days) : (el.getCreated() + 10.days) )
def now = new Date()
if (resolved.format('YYYY') != '1970') {
now = resolved
}
def escalation = 1
escalation = ( now > due - 1.days ? 2 : 1)
escalation = ( now > due + 5.days ? 3 : escalation)
def remaining = (due - now)
remaining = (remaining.getMinutes() > 30 ? remaining + 1.hours : remaining)
remaining = (remaining.getHours() == 24 ? remaining - 24.hours + 1.days: remaining)
remaining = new DatumDependentDuration(0, 0, remaining.getDays(), remaining.getHours(), 0, 0, 0)
remaining = ( due > now ? '+ ' + remaining.toString() : '- ' + remaining.toString().replaceAll('-', '') )
issue.put('key', el.key)
issue.put('summary', (el.summary.length() > maxLen ? el.summary[0..maxLen-1] + '...' : el.summary))
issue.put('status', el.status.name)
issue.put('priority_id', el.priority.id)
issue.put('priority_name', el.priority?.name)
issue.put('created', el.getCreated().format(dFormat) )
issue.put('resolved_date', resolved_date )
issue.put('integration_date', integration_date )
issue.put('production_date', production_date )
issue.put('due', due.format(dFormat) )
issue.put('remaining', remaining )
issue.put('escalation', escalation )
issue.put('assignee', el.assignee?.displayName.replaceAll("\\(Extern\\)", "") )
issue.put('estimate', (el.estimate != null ? Math.round(el.estimate.div(secInHour) * 10) / 10 : 'n/a') )
issues.add(issue)
}
}
} else {
results = "Invalid JQL: $jqlQuery"
return results
}
Map params = [:]
//params.put('issue', myIssue)
params.put('issues', issues)
params.put('TimeCategory', TimeCategory)
params.put('StringUtils', StringUtils)
//params.put('i18n', i18n)
params.put('FIELD_PREVIEW_ISSUE', 'KTINC-2086')
params.put('FIELD_EMAIL_TEMPLATE', template)
params.put('FIELD_EMAIL_SUBJECT_TEMPLATE', 'SLA relevant Incidents')
//params.put('FIELD_TO_ADDRESSES', 'Hubert.Nafalski.extern@KabelDeutschland.de')
params.put('FIELD_TO_ADDRESSES', 'Hubert.Nafalski.extern@KabelDeutschland.de, Martin.Feichtmair@KabelDeutschland.de, Vassili.Andrianov1@KabelDeutschland.de, Sebastian.Hinderer@KabelDeutschland.de, VLNSCEntwicklungDCKSCTSC@KabelDeutschland.de, Artur.Mlynarski.extern@KabelDeutschland.de')
params.put('FIELD_EMAIL_FORMAT', 'HTML')
params.put('FIELD_INCLUDE_ATTACHMENTS', 'FIELD_INCLUDE_ATTACHMENTS_NONE')
SendCustomEmail script = new SendCustomEmail()
script.sendMail(params)
res = script.getDescription(params, true)
return res