PageRenderTime 31ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/resources/com/onresolve/jira/groovy/canned/workflow/postfunctions/SendCustomEmail.groovy

https://bitbucket.org/aschuma/scriptrunner-public-2.0.9
Groovy | 539 lines | 465 code | 55 blank | 19 comment | 74 complexity | 489998aaef18f2f517b24965f51064eb MD5 | raw file
  1. package com.onresolve.jira.groovy.canned.workflow.postfunctions
  2. import com.atlassian.core.ofbiz.CoreFactory
  3. import com.atlassian.crowd.embedded.api.Group
  4. import com.atlassian.crowd.embedded.api.User
  5. import com.atlassian.jira.ComponentManager
  6. import com.atlassian.jira.ManagerFactory
  7. import com.atlassian.jira.config.properties.APKeys
  8. import com.atlassian.jira.config.properties.ApplicationProperties
  9. import com.atlassian.jira.event.issue.IssueEvent
  10. import com.atlassian.jira.issue.CustomFieldManager
  11. import com.atlassian.jira.issue.Issue
  12. import com.atlassian.jira.issue.IssueManager
  13. import com.atlassian.jira.issue.MutableIssue
  14. import com.atlassian.jira.issue.attachment.Attachment
  15. import com.atlassian.jira.issue.customfields.impl.MultiGroupCFType
  16. import com.atlassian.jira.issue.customfields.impl.MultiUserCFType
  17. import com.atlassian.jira.issue.customfields.impl.UserCFType
  18. import com.atlassian.jira.issue.fields.CustomField
  19. import com.atlassian.jira.issue.history.ChangeItemBean
  20. import com.atlassian.jira.issue.watchers.WatcherManager
  21. import com.atlassian.jira.security.groups.GroupManager
  22. import com.atlassian.jira.security.roles.ProjectRole
  23. import com.atlassian.jira.security.roles.ProjectRoleManager
  24. import com.atlassian.jira.user.util.UserUtil
  25. import com.atlassian.jira.util.AttachmentUtils
  26. import com.atlassian.jira.util.ErrorCollection
  27. import com.atlassian.jira.util.SimpleErrorCollection
  28. import com.atlassian.mail.Email
  29. import com.atlassian.mail.MailException
  30. import com.atlassian.mail.MailFactory
  31. import com.atlassian.mail.queue.SingleMailQueueItem
  32. import com.atlassian.mail.server.MailServerManager
  33. import com.atlassian.mail.server.SMTPMailServer
  34. import com.onresolve.jira.groovy.canned.CannedScript
  35. import com.onresolve.jira.groovy.canned.utils.ConditionUtils
  36. import groovy.text.GStringTemplateEngine
  37. import groovy.xml.MarkupBuilder
  38. import org.apache.log4j.Category
  39. import org.ofbiz.core.entity.GenericValue
  40. import java.util.regex.Matcher
  41. import javax.activation.DataHandler
  42. import javax.activation.FileDataSource
  43. import javax.mail.BodyPart
  44. import javax.mail.Multipart
  45. import javax.mail.internet.AddressException
  46. import javax.mail.internet.InternetAddress
  47. import javax.mail.internet.MimeBodyPart
  48. import javax.mail.internet.MimeMultipart
  49. class SendCustomEmail implements CannedScript{
  50. public static String FIELD_PREVIEW_ISSUE = "FIELD_PREVIEW_ISSUE"
  51. public static String FIELD_EMAIL_TEMPLATE = "FIELD_EMAIL_TEMPLATE"
  52. public static String FIELD_EMAIL_FORMAT = "FIELD_EMAIL_FORMAT"
  53. public static String FIELD_TO_ADDRESSES = "FIELD_TO_ADDRESSES"
  54. public static String FIELD_TO_USER_FIELDS = "FIELD_TO_USER_FIELDS"
  55. public static String FIELD_EMAIL_SUBJECT_TEMPLATE = "FIELD_EMAIL_SUBJECT_TEMPLATE"
  56. public static String FIELD_INCLUDE_ATTACHMENTS = "FIELD_INCLUDE_ATTACHMENTS"
  57. public static String FIELD_FROM = "FIELD_FROM"
  58. public static String FIELD_INCLUDE_ATTACHMENTS_NONE = "FIELD_INCLUDE_ATTACHMENTS_NONE"
  59. public static String FIELD_INCLUDE_ATTACHMENTS_NEW = "FIELD_INCLUDE_ATTACHMENTS_NEW"
  60. public static String FIELD_INCLUDE_ATTACHMENTS_ALL = "FIELD_INCLUDE_ATTACHMENTS_ALL"
  61. ComponentManager componentManager = ComponentManager.getInstance()
  62. IssueManager issueManager = componentManager.getIssueManager()
  63. def watcherManager = componentManager.getWatcherManager()
  64. def customFieldManager = componentManager.getCustomFieldManager()
  65. def projectRoleManager = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class)
  66. def groupManager = ComponentManager.getComponentInstanceOfType(GroupManager.class)
  67. def userUtil = componentManager.getUserUtil()
  68. def mailServerManager = componentManager.getMailServerManager()
  69. def mailServer = mailServerManager.getDefaultSMTPMailServer()
  70. Category log = Category.getInstance(SendCustomEmail.class)
  71. String getName() {
  72. "Send a custom email"
  73. }
  74. public String getHelpUrl() {
  75. "https://studio.plugins.atlassian.com/wiki/display/GRV/Built-In+Scripts#Built-InScripts-Sendacustomemail"
  76. }
  77. String getDescription() {
  78. "Send an email based on the provided template if conditions are met"
  79. }
  80. List getCategories() {
  81. ["Function","ADMIN", "Listener"]
  82. }
  83. List getParameters(Map params) {
  84. [
  85. ConditionUtils.getConditionParameter(),
  86. [
  87. Name:FIELD_EMAIL_TEMPLATE,
  88. Label:"Email template",
  89. Description:"""Write a template. Can be plain text or use the
  90. <a href=\"http://docs.codehaus.org/display/GROOVY/Groovy+Templates#GroovyTemplates-GStringTemplateEngine\">GStringTemplateEngine</a>.
  91. See wiki for examples.""",
  92. Type:"text",
  93. ],
  94. [
  95. Name:FIELD_EMAIL_SUBJECT_TEMPLATE,
  96. Label:"Subject template",
  97. Description:"""Subject template. Can be plain text or use the
  98. <a href=\"http://docs.codehaus.org/display/GROOVY/Groovy+Templates#GroovyTemplates-GStringTemplateEngine\">GStringTemplateEngine</a>.
  99. See wiki for examples or click below.""",
  100. Examples: [
  101. """Issue XYZ-1 requires your approval""" : "Issue \$issue requires your approval"
  102. ]
  103. ],
  104. [
  105. Name:FIELD_EMAIL_FORMAT,
  106. Label:"Email format",
  107. Description:"Whether to send as plain text or HTML.",
  108. Type:"list",
  109. Values: [TEXT:'Plain text', HTML:'HTML'],
  110. ],
  111. [
  112. Name:FIELD_TO_ADDRESSES,
  113. Label:"To addresses",
  114. Description:" Who to send the email to. Use commas/space to delimit.",
  115. ],
  116. [
  117. Name:FIELD_TO_USER_FIELDS,
  118. Label:"To issue fields",
  119. Description:""" Who to send the email to - valid issue fields such as assignee, reporter, watchers, user
  120. or user group custom fields,<br> or custom fields holding valid email address,
  121. or role:<i>Rolename</i>, e.g role:Developers, or group:<i>Groupname</i>. Use commas/space to delimit.""",
  122. ],
  123. [
  124. Name:FIELD_INCLUDE_ATTACHMENTS,
  125. Label:"Include attachments",
  126. Type:"radio",
  127. Values: [
  128. (FIELD_INCLUDE_ATTACHMENTS_NONE): "None",
  129. (FIELD_INCLUDE_ATTACHMENTS_NEW): "New",
  130. (FIELD_INCLUDE_ATTACHMENTS_ALL): "All"],
  131. Description: """Include the issue attachments in the email. You can specify none, or only attachments
  132. that were added in the transition pertaining to this event, or all attachments."""
  133. ],
  134. [
  135. Name:FIELD_FROM,
  136. Label:"From email address",
  137. Description:""" What email address to send the mail from, eg jamie@example.com.
  138. Leave blank for default (<i>${mailServer?.getDefaultFrom()}</i>).""",
  139. ],
  140. [
  141. Name:FIELD_PREVIEW_ISSUE,
  142. Label:"Preview Issue Key",
  143. Description:"""Issue key for previewing what the mail will look like.
  144. ONLY used when previewing from the Admin section""",
  145. ],
  146. ]
  147. }
  148. ErrorCollection doValidate(Map params, boolean forPreview) {
  149. ErrorCollection errorCollection = new SimpleErrorCollection()
  150. String prvwIssueKey = params[FIELD_PREVIEW_ISSUE] as String
  151. if (forPreview) {
  152. if (! issueManager.getIssueObject(prvwIssueKey as String))
  153. errorCollection.addError(FIELD_PREVIEW_ISSUE, "This issue doesn't exist.")
  154. String cond = params[ConditionUtils.FIELD_CONDITION] as String
  155. if (cond) {
  156. try {
  157. MutableIssue issue = issueManager.getIssueObject(prvwIssueKey)
  158. ConditionUtils.processCondition(cond, issue, true,
  159. [event: new IssueEvent(issue, [:] , null, 1)])
  160. /*
  161. {
  162. def getChangeLog() {
  163. [getRelated : {[]}]
  164. }
  165. })
  166. */
  167. }
  168. catch (Exception e) {
  169. errorCollection.addError(ConditionUtils.FIELD_CONDITION, e.getMessage())
  170. log.debug(e.getMessage())
  171. }
  172. }
  173. }
  174. if (! params[FIELD_EMAIL_TEMPLATE]) {
  175. errorCollection.addError(FIELD_EMAIL_TEMPLATE, "You must provide a template.")
  176. }
  177. if (! params[FIELD_EMAIL_SUBJECT_TEMPLATE]) {
  178. errorCollection.addError(FIELD_EMAIL_SUBJECT_TEMPLATE, "You must provide a subject.")
  179. }
  180. if (! (params[FIELD_TO_ADDRESSES] || params[FIELD_TO_USER_FIELDS])) {
  181. errorCollection.addErrorMessage ("You must provide either fields or addresses to send emails to.")
  182. }
  183. if (params[FIELD_TO_ADDRESSES]) {
  184. def List invalid = getTextAddresses(params[FIELD_TO_ADDRESSES] as String).asList().findAll {String a ->
  185. ! validateEmail(a)
  186. }
  187. if (invalid)
  188. errorCollection.addError(FIELD_TO_ADDRESSES, "These don't look like valid email addresses: " + invalid.join(", "))
  189. }
  190. if (params[FIELD_FROM]) {
  191. if (! validateEmail(params[FIELD_FROM] as String)) {
  192. errorCollection.addError(FIELD_FROM, "Email is not valid: ${params[FIELD_FROM]}")
  193. }
  194. }
  195. errorCollection
  196. }
  197. private boolean validateEmail (String email) {
  198. // don't use EmailValidator.getInstance().isValid, not worth the trouble of importing the classes
  199. try {
  200. new InternetAddress(email).validate()
  201. }
  202. catch (AddressException ae) {
  203. return false
  204. }
  205. true
  206. }
  207. Map doScript(Map params) {
  208. MutableIssue issue = params['issue'] as MutableIssue
  209. // preview mode
  210. if (!issue) {
  211. issue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
  212. }
  213. Boolean doIt = ConditionUtils.processCondition(params[ConditionUtils.FIELD_CONDITION] as String, issue, false, params)
  214. if (! doIt) {
  215. return [:]
  216. }
  217. // do validation again
  218. String emailFormat = params[FIELD_EMAIL_FORMAT]
  219. log.debug("emailFormat: $emailFormat")
  220. if (mailServer && ! MailFactory.isSendingDisabled()) {
  221. Writable template = mergeEmailTemplateBody(params)
  222. String body = template.toString()
  223. for (String address in getAllAddresses(issue, params)) {
  224. Email email = new Email(address)
  225. // Now the message body.
  226. addAttachmentsToMail(params, issue, email)
  227. email.setFrom(params[FIELD_FROM] as String ?: mailServer.getDefaultFrom())
  228. email.setSubject(mergeEmailTemplateSubject(params).toString())
  229. email.setMimeType(emailFormat == "HTML" ? "text/html" : "text/plain")
  230. // could use addheader to set all the to list on one line
  231. email.setBody(body)
  232. try {
  233. log.debug ("Sending mail to ${email.getTo()}")
  234. log.debug ("with body ${email.getBody()}")
  235. log.debug ("from template ${params[FIELD_EMAIL_TEMPLATE]}")
  236. SingleMailQueueItem item = new SingleMailQueueItem(email);
  237. ManagerFactory.getMailQueue().addItem(item);
  238. }
  239. catch (MailException e) {
  240. log.warn ("Error sending email", e)
  241. }
  242. }
  243. }
  244. else {
  245. log.warn ("No mail server or sending disabled.")
  246. }
  247. params.remove("event")
  248. return params
  249. }
  250. private def addAttachmentsToMail(Map params, MutableIssue issue, Email email) {
  251. if (params[FIELD_INCLUDE_ATTACHMENTS] && params[FIELD_INCLUDE_ATTACHMENTS] != FIELD_INCLUDE_ATTACHMENTS_NONE) {
  252. Multipart mp = new MimeMultipart("mixed");
  253. List attachmentIds = []
  254. if (params[FIELD_INCLUDE_ATTACHMENTS] == FIELD_INCLUDE_ATTACHMENTS_NEW) {
  255. if (!(params["event"] || params["transientVars"])) {
  256. params.put("event", fakeLatestEvent(issue))
  257. log.debug("No event or transient vars - must be admin screen mode - creating latest event: " + params.get("event"))
  258. }
  259. if (params["event"]) { // listener
  260. List changeItems = params["event"].getChangeLog()?.getRelated("ChildChangeItem")
  261. log.debug("changeItems: $changeItems")
  262. changeItems.each {GenericValue gv ->
  263. if (gv["field"] == "Attachment" && gv["newvalue"]) {
  264. attachmentIds.add(gv["newvalue"])
  265. }
  266. }
  267. }
  268. else if (params["transientVars"]) {
  269. List changeItems = params["transientVars"]["changeItems"] as List
  270. changeItems.each {ChangeItemBean cib ->
  271. if (cib.getField() == "Attachment" && cib.getTo()) {
  272. attachmentIds.add(cib.getTo())
  273. }
  274. }
  275. }
  276. }
  277. else {
  278. attachmentIds = issue.getAttachments()*.id
  279. }
  280. attachmentIds.each {attachmentId ->
  281. Attachment attachment = issue.attachments.find {Attachment attachment ->
  282. attachment.id == new Long(attachmentId)
  283. }
  284. File attFile = AttachmentUtils.getAttachmentFile(attachment)
  285. BodyPart attPart = new MimeBodyPart()
  286. FileDataSource attFds = new FileDataSource(attFile)
  287. attPart.setDataHandler(new DataHandler(attFds))
  288. attPart.setFileName(attachment.filename)
  289. log.debug("Attaching ${attachment.filename} to mail")
  290. mp.addBodyPart(attPart)
  291. }
  292. email.setMultipart(mp)
  293. }
  294. }
  295. Set getTextAddresses(String toConfig) {
  296. Set addresses = new HashSet()
  297. if (toConfig) {
  298. for (String f in toConfig.split(/[\s,;]+/)) {
  299. addresses.add(f)
  300. }
  301. }
  302. addresses
  303. }
  304. List getAllAddresses(Issue issue, Map params) {
  305. String toUserFields = params[FIELD_TO_USER_FIELDS]
  306. Set addresses = getAddressesFromFields(issue, toUserFields)
  307. addresses.addAll(getTextAddresses(params[FIELD_TO_ADDRESSES] as String))
  308. addresses.toList()
  309. }
  310. Set getAddressesFromFields(Issue issue, String toConfig) {
  311. Set addresses = new HashSet()
  312. // for testing: "reporter,assignee, watchers, customfield_10020, customfield_10040, customfield_10041, customfield_10042, customfield_10043, group:"a b""
  313. String patStr = /([^\s]*"[^"]*")|([^\s"']+)/
  314. Matcher matcher = toConfig =~ patStr
  315. log.debug("toConfig: \"$toConfig\"")
  316. if (matcher.getCount() == 0) {
  317. return []
  318. }
  319. for (int i in 0..matcher.getCount()-1) {
  320. String f = matcher[i][0]
  321. if (f) {
  322. f = f.trim()
  323. log.debug ("field f: \"$f\"")
  324. if(['reporter', 'assignee'].contains(f)) {
  325. addresses.add((issue.getAt(f) as User)?.emailAddress)
  326. }
  327. else if (f == "watchers") {
  328. watcherManager.getCurrentWatcherUsernames(issue.genericValue).each {String username ->
  329. addresses.add(userUtil.getUser(username).getEmailAddress())
  330. }
  331. // doesn't work in jira 4.x
  332. // addresses.addAll(watcherManager.getCurrentWatchList(issue.genericValue).collect {it.email})
  333. }
  334. else if (f.toLowerCase().startsWith("customfield_")) {
  335. CustomField cf = customFieldManager.getCustomFieldObjects(issue).find {it.id == f} as CustomField
  336. if (cf.getCustomFieldType() instanceof UserCFType) {
  337. addresses.add(cf.getValue(issue)?.emailAddress)
  338. }
  339. else if (cf.getCustomFieldType() instanceof MultiUserCFType) {
  340. addresses.addAll(cf.getValue(issue)?.collect{it.email})
  341. }
  342. else if (cf.getCustomFieldType() instanceof MultiGroupCFType) {
  343. addresses.addAll (cf.getValue(issue).collect {Group group ->
  344. groupManager.getUsersInGroup(group).collect {userUtil.getUser(it as String)?.emailAddress}
  345. }.flatten())
  346. }
  347. else if (isTextField(cf)) {
  348. addresses.addAll((cf.getValue(issue) as String).split(/[\s,;]+/))
  349. }
  350. else {
  351. log.warn("Unhandled custom field type $f")
  352. }
  353. }
  354. else if (f.toLowerCase().startsWith("role:")) {
  355. f = f.replaceFirst("role:", "")
  356. f = f.replaceAll("\"", "")
  357. ProjectRole role = projectRoleManager.getProjectRole(f)
  358. if (role) {
  359. addresses.addAll (projectRoleManager.getProjectRoleActors(role, issue.projectObject).getUsers()*.emailAddress)
  360. }
  361. else {
  362. log.warn ("Could not find role named \"$f\"")
  363. }
  364. }
  365. else if (f.toLowerCase().startsWith("group:")) {
  366. f = f.replaceFirst("group:", "")
  367. f = f.replaceAll("\"", "")
  368. Group group = userUtil.getGroup(f)
  369. if (group) {
  370. addresses.addAll(groupManager.getUsersInGroup(group)*.emailAddress)
  371. log.debug "addresses: $addresses"
  372. }
  373. else {
  374. log.warn ("Could not find group named \"$f\"")
  375. }
  376. }
  377. else {
  378. log.warn ("Could not handle field $f")
  379. }
  380. }
  381. }
  382. addresses.minus([null])
  383. }
  384. // https://studio.plugins.atlassian.com/browse/GRV-102
  385. Boolean isTextField (CustomField cf) {
  386. return ["com.atlassian.jira.issue.customfields.impl.TextCFType",
  387. "com.atlassian.jira.issue.customfields.impl.GenericTextCFType"].any {
  388. try {
  389. return Class.forName(it).isAssignableFrom(cf.getCustomFieldType().class)
  390. }
  391. catch (Exception e) {
  392. return false
  393. }
  394. }
  395. }
  396. String getDescription(Map params, boolean forPreview) {
  397. if (!forPreview) {
  398. return getDescription()
  399. }
  400. Writable template = mergeEmailTemplateBody(params, true)
  401. String emailFormat = params[FIELD_EMAIL_FORMAT]
  402. MutableIssue prvwIssue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
  403. // add the changeitems for the most recent event so listeners can be better tested
  404. params.put("event", fakeLatestEvent(prvwIssue))
  405. List sendTo = getAllAddresses(prvwIssue, params)
  406. Boolean condition = ConditionUtils.processCondition(params[ConditionUtils.FIELD_CONDITION] as String, prvwIssue, false)
  407. StringWriter writer = new StringWriter()
  408. MarkupBuilder builder = new MarkupBuilder(writer)
  409. builder.table {
  410. tr{
  411. td{b("Condition")}
  412. td("The condition evaluated to $condition (note that for listeners the condition can't normally be tested)")
  413. }
  414. tr{
  415. td{b("To")}
  416. td(style:'background-color:#ffffff', sendTo.join(", "))
  417. }
  418. tr{
  419. td{b("From")}
  420. td(style:'background-color:#ffffff', params[FIELD_FROM] as String ?: mailServer.getDefaultFrom())
  421. }
  422. tr{
  423. td{b("Subject")}
  424. td(style:'background-color:#ffffff', mergeEmailTemplateSubject(params, true).toString())
  425. }
  426. tr{
  427. td(valign:'top') {
  428. b("Body")
  429. }
  430. td(style:'background-color:#ffffff'){
  431. if (emailFormat == 'TEXT') {
  432. pre(template.toString())
  433. }
  434. else {
  435. p(mkp.yieldUnescaped (template.toString()))
  436. }
  437. }
  438. }
  439. }
  440. // remove the event other it goes in to the page as a String
  441. params.remove("event")
  442. writer.toString()
  443. }
  444. private IssueEvent fakeLatestEvent(MutableIssue prvwIssue) {
  445. Collection<GenericValue> changeGroups = CoreFactory.getGenericDelegator().findByAnd("ChangeGroup", ["issue": prvwIssue.getId()]);
  446. IssueEvent event
  447. if (changeGroups) {
  448. def changeGroup = changeGroups ? changeGroups.last() : null
  449. event = new IssueEvent(prvwIssue, null, null, null, changeGroup, [:], 1, false)
  450. }
  451. else {
  452. event = new IssueEvent(prvwIssue, [:] , null, 1)
  453. }
  454. return event
  455. }
  456. public Writable mergeEmailTemplateBody(Map params, Boolean isPreview = false) {
  457. mergeEmailTemplate(params, params[FIELD_EMAIL_TEMPLATE] as String, isPreview)
  458. }
  459. public Writable mergeEmailTemplateSubject(Map params, Boolean isPreview = false) {
  460. mergeEmailTemplate(params, params[FIELD_EMAIL_SUBJECT_TEMPLATE] as String, isPreview)
  461. }
  462. private Writable mergeEmailTemplate(Map params, String template, Boolean isPreview = false) {
  463. GStringTemplateEngine engine = new GStringTemplateEngine()
  464. Map binding = [:]
  465. binding.putAll(params)
  466. MutableIssue issue
  467. if (params["issue"]) {
  468. issue = params["issue"] as MutableIssue
  469. }
  470. else {
  471. issue = issueManager.getIssueObject(params[FIELD_PREVIEW_ISSUE] as String)
  472. }
  473. // todo document new things in the binding
  474. ApplicationProperties applicationProperties = componentManager.getApplicationProperties()
  475. binding.put("baseUrl", applicationProperties.getString(APKeys.JIRA_BASEURL))
  476. binding.put("componentManager", componentManager)
  477. binding.put("issue", issue)
  478. binding.putAll(ConditionUtils.setupBinding(issue, binding))
  479. engine.createTemplate(template).make(binding)
  480. }
  481. public Boolean isFinalParamsPage(Map params) {
  482. true
  483. }
  484. }