/src/main/resources/com/onresolve/jira/groovy/canned/workflow/postfunctions/SendCustomEmail.groovy
Groovy | 539 lines | 465 code | 55 blank | 19 comment | 74 complexity | 489998aaef18f2f517b24965f51064eb MD5 | raw file
- 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.ManagerFactory
- 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.CustomFieldManager
- import com.atlassian.jira.issue.Issue
- import com.atlassian.jira.issue.IssueManager
- import com.atlassian.jira.issue.MutableIssue
- 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.issue.watchers.WatcherManager
- 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.user.util.UserUtil
- 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.atlassian.mail.server.MailServerManager
- import com.atlassian.mail.server.SMTPMailServer
- import com.onresolve.jira.groovy.canned.CannedScript
- import com.onresolve.jira.groovy.canned.utils.ConditionUtils
- import groovy.text.GStringTemplateEngine
- import groovy.xml.MarkupBuilder
- import org.apache.log4j.Category
- import org.ofbiz.core.entity.GenericValue
-
- import java.util.regex.Matcher
- 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
-
- 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_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"
-
- ComponentManager componentManager = ComponentManager.getInstance()
- IssueManager issueManager = componentManager.getIssueManager()
- def watcherManager = componentManager.getWatcherManager()
- def customFieldManager = componentManager.getCustomFieldManager()
- def projectRoleManager = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class)
- def groupManager = ComponentManager.getComponentInstanceOfType(GroupManager.class)
- def userUtil = componentManager.getUserUtil()
- def mailServerManager = componentManager.getMailServerManager()
- def mailServer = mailServerManager.getDefaultSMTPMailServer()
-
- Category log = Category.getInstance(SendCustomEmail.class)
-
- String getName() {
- "Send a custom email"
- }
-
- public String getHelpUrl() {
- "https://studio.plugins.atlassian.com/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 params) {
- [
- ConditionUtils.getConditionParameter(),
- [
- Name:FIELD_EMAIL_TEMPLATE,
- Label:"Email template",
- Description:"""Write a template. Can be plain text or use the
- <a href=\"http://docs.codehaus.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://docs.codehaus.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:""" Who to send the email to - valid issue fields such as assignee, reporter, watchers, user
- or user group custom fields,<br> or custom fields holding valid email address,
- or role:<i>Rolename</i>, e.g role:Developers, or group:<i>Groupname</i>. Use commas/space to delimit.""",
- ],
- [
- 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"],
- 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_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""",
- ],
- ]
- }
-
- 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.")
- }
- if (params[FIELD_TO_ADDRESSES]) {
- def List invalid = getTextAddresses(params[FIELD_TO_ADDRESSES] as String).asList().findAll {String a ->
- ! validateEmail(a)
- }
- if (invalid)
- errorCollection.addError(FIELD_TO_ADDRESSES, "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]}")
- }
- }
- 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
- }
-
- Map doScript(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()) {
- Writable template = mergeEmailTemplateBody(params)
- String body = template.toString()
- for (String address in getAllAddresses(issue, params)) {
- Email email = new Email(address)
- // 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")
- // could use addheader to set all the to list on one line
- 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);
- ManagerFactory.getMailQueue().addItem(item);
- }
- catch (MailException e) {
- log.warn ("Error sending email", e)
- }
- }
- }
- else {
- log.warn ("No mail server or sending disabled.")
- }
-
- params.remove("event")
- return params
- }
-
- private def addAttachmentsToMail(Map params, MutableIssue issue, Email email) {
- if (params[FIELD_INCLUDE_ATTACHMENTS] && params[FIELD_INCLUDE_ATTACHMENTS] != FIELD_INCLUDE_ATTACHMENTS_NONE) {
- Multipart mp = new MimeMultipart("mixed");
- List attachmentIds = []
-
- 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"]) {
- attachmentIds.add(gv["newvalue"])
- }
- }
- }
- else if (params["transientVars"]) {
- List changeItems = params["transientVars"]["changeItems"] as List
- changeItems.each {ChangeItemBean cib ->
- if (cib.getField() == "Attachment" && cib.getTo()) {
- attachmentIds.add(cib.getTo())
- }
- }
- }
- }
- else {
- attachmentIds = issue.getAttachments()*.id
- }
-
- attachmentIds.each {attachmentId ->
- Attachment attachment = issue.attachments.find {Attachment attachment ->
- attachment.id == new Long(attachmentId)
- }
- 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)
- }
- }
-
- Set getTextAddresses(String toConfig) {
-
- Set addresses = new HashSet()
- if (toConfig) {
- for (String f in toConfig.split(/[\s,;]+/)) {
- addresses.add(f)
- }
- }
- addresses
- }
-
- List getAllAddresses(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()
- }
-
- Set getAddressesFromFields(Issue issue, String toConfig) {
- Set addresses = new HashSet()
-
- // 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).getEmailAddress())
- }
- // doesn't work in jira 4.x
- // addresses.addAll(watcherManager.getCurrentWatchList(issue.genericValue).collect {it.email})
- }
- 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.email})
- }
- 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)) {
- addresses.addAll((cf.getValue(issue) as String).split(/[\s,;]+/))
- }
- else {
- log.warn("Unhandled custom field type $f")
- }
- }
- 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://studio.plugins.atlassian.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()
- }
-
- 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))
-
- List sendTo = getAllAddresses(prvwIssue, params)
-
- 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("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(valign:'top') {
- b("Body")
- }
- td(style:'background-color:#ffffff'){
- if (emailFormat == 'TEXT') {
- pre(template.toString())
- }
- else {
- p(mkp.yieldUnescaped (template.toString()))
- }
- }
- }
- }
-
- // remove the event other it goes in to the page as a String
- params.remove("event")
- 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("componentManager", componentManager)
- binding.put("issue", issue)
- binding.putAll(ConditionUtils.setupBinding(issue, binding))
-
- engine.createTemplate(template).make(binding)
- }
-
- public Boolean isFinalParamsPage(Map params) {
- true
- }
- }