PageRenderTime 571ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/src/main/java/com/redradishtech/jira/ccmailer/CCMailerListener.java

https://bitbucket.org/redradish/ccmailer-44x
Java | 718 lines | 552 code | 65 blank | 101 comment | 69 complexity | abf1e406485d8c8801adf70b70eb1460 MD5 | raw file
  1. package com.redradishtech.jira.ccmailer;
  2. import com.atlassian.core.ofbiz.util.OFBizPropertyUtils;
  3. import com.atlassian.core.util.StringUtils;
  4. import com.atlassian.jira.ComponentManager;
  5. import com.atlassian.jira.ManagerFactory;
  6. import com.atlassian.jira.config.properties.APKeys;
  7. import com.atlassian.jira.config.properties.ApplicationProperties;
  8. import com.atlassian.jira.event.issue.AbstractIssueEventListener;
  9. import com.atlassian.jira.event.issue.IssueEvent;
  10. import com.atlassian.jira.event.issue.IssueEventListener;
  11. import com.atlassian.jira.event.type.EventType;
  12. import com.atlassian.jira.event.type.EventTypeManager;
  13. import com.atlassian.jira.issue.CustomFieldManager;
  14. import com.atlassian.jira.issue.Issue;
  15. import com.atlassian.jira.issue.IssueManager;
  16. import com.atlassian.jira.issue.RendererManager;
  17. import com.atlassian.jira.issue.attachment.Attachment;
  18. import com.atlassian.jira.issue.comments.Comment;
  19. import com.atlassian.jira.issue.fields.CustomField;
  20. import com.atlassian.jira.issue.fields.renderer.wiki.AtlassianWikiRenderer;
  21. import com.atlassian.jira.mail.Email;
  22. import com.atlassian.jira.mail.JiraMailThreader;
  23. import com.atlassian.jira.notification.NotificationSchemeManager;
  24. import com.atlassian.jira.project.ProjectKeys;
  25. import com.atlassian.jira.security.PermissionManager;
  26. import com.atlassian.jira.security.roles.ProjectRole;
  27. import com.atlassian.jira.util.AttachmentUtils;
  28. import com.atlassian.jira.util.NotNull;
  29. import com.atlassian.jira.util.URLCodec;
  30. import com.atlassian.mail.MailThreader;
  31. import com.atlassian.mail.queue.SingleMailQueueItem;
  32. import com.atlassian.velocity.VelocityManager;
  33. import com.opensymphony.user.User;
  34. import com.redradishtech.jira.ccmailer.util.AttachmentLinkRewriter;
  35. import com.redradishtech.jira.ccmailer.util.CustomFieldHelperBean;
  36. import org.apache.commons.lang.builder.ToStringBuilder;
  37. import org.apache.log4j.Logger;
  38. import org.apache.log4j.NDC;
  39. import org.apache.velocity.exception.VelocityException;
  40. import org.ofbiz.core.entity.GenericEntityException;
  41. import org.ofbiz.core.entity.GenericValue;
  42. import javax.activation.DataHandler;
  43. import javax.activation.FileDataSource;
  44. import javax.mail.MessagingException;
  45. import javax.mail.Multipart;
  46. import javax.mail.Part;
  47. import javax.mail.internet.InternetAddress;
  48. import javax.mail.internet.MimeBodyPart;
  49. import javax.mail.internet.MimeMultipart;
  50. import javax.mail.internet.MimeUtility;
  51. import java.io.File;
  52. import java.io.UnsupportedEncodingException;
  53. import java.util.*;
  54. import java.util.concurrent.atomic.AtomicReference;
  55. import java.util.regex.Matcher;
  56. import java.util.regex.Pattern;
  57. import java.util.regex.PatternSyntaxException;
  58. /**
  59. * Listener that sends an email notification out to addresses listed in a CC custom field (whose name/ID is configurable).
  60. * <p/>
  61. * This listener only triggers if the project's notification scheme notifies All Watchers of the event type.
  62. * It is possible to prevent CCed users getting a comment update by setting the comment's security level (to anything).
  63. * <p/>
  64. * The listener can be configured to only trigger for certain projects, by providing a comma-separated list of project keys.
  65. * The listener can be disabled altogether with the 'disable.ccmailer.notifications' system property.
  66. */
  67. public class CCMailerListener extends AbstractIssueEventListener implements IssueEventListener
  68. {
  69. private static final Logger log = Logger.getLogger(CCMailerListener.class);
  70. private static final String DISABLE_CCMAILER_NOTIFICATIONS_PROPERTY = "disable.ccmailer.notifications";
  71. private static final boolean CCMAILER_NOTIFICATIONS_DISABLED = "true".equalsIgnoreCase(System.getProperty(DISABLE_CCMAILER_NOTIFICATIONS_PROPERTY, "false"));
  72. static final String PARAM_SPLIT_REGEX = "(?<!\\\\),"; // split on an unescaped comma. The regexp is a zero-width negative lookbehind assertion.
  73. static final String CC_CUSTOMFIELD_ID = "CC custom field ID";
  74. protected static final String PROJECT_KEYS = "Only notify for issues in projects with these keys (default: all projects)";
  75. protected static final String TRIGGER_EVENTS = "Events triggering message (default: all events)";
  76. static final String IGNORED_ME = "Ignore events generated by my own changes true/false (default: true)";
  77. protected Set<String> projectKeys = new HashSet<String>();
  78. private boolean ignoreMyEvents = true;
  79. private CustomField ccCustomField;
  80. protected IssueManager issueManager;
  81. protected PermissionManager permissionManager;
  82. private VelocityManager velocityManager;
  83. private CustomFieldManager customFieldManager;
  84. private NotificationSchemeManager notificationSchemeManager;
  85. private RendererManager rendererManager;
  86. private EventTypeManager eventTypeManager;
  87. private ApplicationProperties applicationProperties;
  88. private final String EMAIL_FROM_REGEXP = "templates/ccmailer/notify_sender_regexp.vm";
  89. private final String EMAIL_TEMPLATE_TEXT = "templates/ccmailer/notify_mailbody_textplain.vm";
  90. private final String EMAIL_TEMPLATE_HTML = "templates/ccmailer/notify_mailbody_texthtml.vm";
  91. private final String EMAIL_SUBJECT_TEMPLATE = "templates/ccmailer/notify_subject.vm";
  92. private HashSet<Long> triggerEventIDs;
  93. public CCMailerListener()
  94. {
  95. this(null, null, null, null, null, null, null, null);
  96. }
  97. public CCMailerListener(IssueManager issueManager, PermissionManager permissionManager, VelocityManager velocityManager,
  98. CustomFieldManager customFieldManager,
  99. NotificationSchemeManager notificationSchemeManager, RendererManager rendererManager, EventTypeManager eventTypeManager,
  100. ApplicationProperties applicationProperties)
  101. {
  102. this.issueManager = issueManager;
  103. this.permissionManager = permissionManager;
  104. this.velocityManager = velocityManager;
  105. this.customFieldManager = customFieldManager;
  106. this.notificationSchemeManager = notificationSchemeManager;
  107. this.rendererManager = rendererManager;
  108. this.eventTypeManager = eventTypeManager;
  109. this.applicationProperties = applicationProperties;
  110. }
  111. public String[] getAcceptedParams()
  112. {
  113. List<String> params = new ArrayList<String>();
  114. params.addAll(Arrays.asList(getAcceptedListenerParams()));
  115. return params.toArray(new String[params.size()]);
  116. }
  117. public void init(Map params)
  118. {
  119. if (CCMAILER_NOTIFICATIONS_DISABLED)
  120. return;
  121. initListener(params);
  122. }
  123. public String toString()
  124. {
  125. return new ToStringBuilder(this).append("ccField", ccCustomField).append("projectKeys", projectKeys).append("triggerEvents", triggerEventIDs).append(DISABLE_CCMAILER_NOTIFICATIONS_PROPERTY, CCMAILER_NOTIFICATIONS_DISABLED).toString();
  126. }
  127. /**
  128. * Initialise listener.
  129. *
  130. * @param params Parameters entered by administrator. If a param was left blank we rely on the caller not to pass it through.
  131. */
  132. protected void initListener(Map params)
  133. {
  134. if (params.containsKey(CC_CUSTOMFIELD_ID))
  135. {
  136. String paramStr = (String) params.get(CC_CUSTOMFIELD_ID);
  137. ccCustomField = new CustomFieldHelperBean(customFieldManager, log).lookupCustomField(paramStr);
  138. }
  139. if (params.containsKey(TRIGGER_EVENTS))
  140. {
  141. String[] eventStrs = ((String) params.get(TRIGGER_EVENTS)).split(PARAM_SPLIT_REGEX); // any unescaped comma
  142. if (eventStrs.length > 0) triggerEventIDs = new HashSet<Long>();
  143. for (String id : eventStrs)
  144. {
  145. id = id.trim();
  146. Long eventId = null;
  147. try
  148. {
  149. eventId = Long.parseLong(id);
  150. } catch (NumberFormatException nfe)
  151. {
  152. for (Object o : eventTypeManager.getEventTypes())
  153. {
  154. EventType e = (EventType) o;
  155. if (id.equals(e.getName())) eventId = e.getId();
  156. }
  157. if (eventId == null)
  158. log.error("CCMailer listener configured to trigger on event '" + id + "', which is not a valid event ID or name.");
  159. }
  160. triggerEventIDs.add(eventId);
  161. }
  162. }
  163. if (params.containsKey(PROJECT_KEYS))
  164. {
  165. String[] projectKeyStrs = ((String) params.get(PROJECT_KEYS)).split(PARAM_SPLIT_REGEX);
  166. for (String projectStr : projectKeyStrs)
  167. {
  168. if (!"".equals(projectStr.trim()))
  169. {
  170. projectKeys.add(projectStr.trim());
  171. }
  172. }
  173. }
  174. if (params.containsKey(IGNORED_ME))
  175. {
  176. // true by default
  177. ignoreMyEvents = !"false".equals(params.get(IGNORED_ME));
  178. }
  179. if (params.size() > 0 && ccCustomField == null)
  180. {
  181. log.warn("No CC field set; no-one will be notified from this listener.");
  182. }
  183. log.info("Initialized listener " + this);
  184. // No error handling, as this method is only called when the listener is first triggered (not when it is configured)
  185. // when no user feedback is possible. Listeners suck..
  186. }
  187. public String[] getAcceptedListenerParams()
  188. {
  189. return new String[]{CC_CUSTOMFIELD_ID, PROJECT_KEYS, TRIGGER_EVENTS, IGNORED_ME};
  190. }
  191. public void workflowEvent(IssueEvent event)
  192. {
  193. if (CCMAILER_NOTIFICATIONS_DISABLED)
  194. return;
  195. if (event.getIssue() == null)
  196. {
  197. log.warn("Event " + event + " has no issue; ignoring");
  198. return;
  199. }
  200. if (ccCustomField == null)
  201. {
  202. log.error("Ignoring event, as no CC custom field configured. You need to set the '" + CC_CUSTOMFIELD_ID + "' parameter on the CCMailer Listener.");
  203. return;
  204. }
  205. final String eventKey = "Evnt:" + event.getRemoteUser() + "->" + event.getEventTypeId() + "@" + event.getIssue().getKey();
  206. NDC.push(eventKey); // Add "(%x)" to your log4j.properties patterns to see this.
  207. try
  208. {
  209. if (meetsTriggerConditions(event))
  210. {
  211. Set<InternetAddress> recipients = getRecipients(event.getIssue());
  212. if (!recipients.isEmpty())
  213. log.debug("Event matched conditions. Potentially notifying " + recipients.size() + " CC users.");
  214. for (InternetAddress recipient : recipients)
  215. {
  216. if (recipientAlreadyNotifiedAsWatcher(event, recipient))
  217. {
  218. log.info("\tUser " + recipient + " is already a watcher; ignoring CC list entry");
  219. }
  220. else if (recipientCreatedEvent(event, recipient) && ignoreMyEvents)
  221. {
  222. log.info("\tUser " + recipient + " created the event and listener is configured to ignore own events; ignoring CC list entry");
  223. }
  224. else
  225. {
  226. if (hasPermission(event, recipient))
  227. {
  228. log.debug(recipient + " was not already notified as a watcher, is not the creator of the event, and has permission to see it.");
  229. log.debug("\tNotifying " + recipient);
  230. String unsubToken = generateUnsubscribeToken(event);
  231. sendMessage(event, recipient, unsubToken);
  232. }
  233. else
  234. {
  235. log.info("\tUser " + recipient + " does not have permission to see comment " + event);
  236. }
  237. }
  238. }
  239. }
  240. } finally
  241. {
  242. NDC.pop();
  243. }
  244. }
  245. /**
  246. * If an CC email address resulted in the creation of this event's comment, we don't want to then send an email notification
  247. * back to that email. This method identifies whether a particular email address triggered the event. It does this by
  248. * parsing the comment. For example if the comment contains 'From: joe@example.com', and recipient is joe@example.com, then
  249. * this returns true. The actual regexp structure is stored in a velocity template to match the templated nature of the
  250. * comment format.
  251. *
  252. * @param event Event
  253. * @param recipient Email address
  254. * @return Whether the recipient email address was the cause of this event.
  255. */
  256. boolean recipientCreatedEvent(IssueEvent event, InternetAddress recipient)
  257. {
  258. Comment comment = event.getComment();
  259. if (comment == null) return false; // Eg. when issue is edited and no comment added
  260. Map<String, Object> params = new HashMap<String, Object>();
  261. params.put("email", Pattern.quote(recipient.getAddress()));
  262. try
  263. {
  264. String senderRegexp = velocityManager.getBody("", EMAIL_FROM_REGEXP, params);
  265. try
  266. {
  267. Pattern p = Pattern.compile(senderRegexp);
  268. Matcher m = p.matcher(comment.getBody());
  269. return m.find();
  270. } catch (PatternSyntaxException pse)
  271. {
  272. log.error("String in " + EMAIL_TEMPLATE_HTML + " is not a valid regular expression after interpolation: " + senderRegexp);
  273. }
  274. } catch (VelocityException e)
  275. {
  276. log.error("Error parsing " + EMAIL_TEMPLATE_HTML, e);
  277. }
  278. return false;
  279. }
  280. /**
  281. * Generates a unique token which we can associate with the event's issue, but will not be guessable. This will be embedded
  282. * in outgoing emails' unsubscribe links, and also stored in the notificationinstance table of the database. The unsubscribe
  283. * action then verifies that it is in the database, to validate that the submitter is the email recipient.
  284. *
  285. * @param event Issue event
  286. * @return Token associated with event.
  287. */
  288. private String generateUnsubscribeToken(IssueEvent event)
  289. {
  290. return "" + System.currentTimeMillis();
  291. }
  292. /**
  293. * Creates and enqueues a CC notification email.
  294. *
  295. * @param event Event to notify the CCed user of
  296. * @param recipient The CC recipient of the email
  297. * @param unsubToken A non-guessable token uniquely identifying the outgoing email, used to verify the unsubscribe requests.
  298. */
  299. private void sendMessage(final IssueEvent event, InternetAddress recipient, String unsubToken)
  300. {
  301. // sends the message by email
  302. String sender = OFBizPropertyUtils.getPropertySet(event.getIssue().getProject()).getString(ProjectKeys.EMAIL_SENDER);
  303. Map<String, Object> params = new HashMap<String, Object>();
  304. params.put("issue", event.getIssue());
  305. String emailSubject = null;
  306. try
  307. {
  308. emailSubject = velocityManager.getBody("", EMAIL_SUBJECT_TEMPLATE, params);
  309. } catch (VelocityException e)
  310. {
  311. log.error("Error rendering CC email subject from template " + EMAIL_SUBJECT_TEMPLATE, e);
  312. }
  313. try
  314. {
  315. Email email = new Email(recipient.getAddress());
  316. email.setFrom(sender);
  317. email.setFromName(getSenderFrom(event));
  318. email.setSubject(emailSubject == null ? "" : emailSubject);
  319. email.addHeader("X-JIRA-IssueKey", event.getIssue().getKey());
  320. final Multipart multipart = new MimeMultipart("alternative");
  321. final MimeBodyPart textPart = new MimeBodyPart();
  322. final MimeBodyPart htmlAndAttachmentsPart = new MimeBodyPart();
  323. final MimeMultipart htmlAndAttachmentsMultipart = new MimeMultipart("related");
  324. final MimeBodyPart htmlPart = new MimeBodyPart();
  325. email.setMultipart(multipart);
  326. multipart.addBodyPart(textPart);
  327. multipart.addBodyPart(htmlAndAttachmentsPart);
  328. htmlAndAttachmentsPart.setContent(htmlAndAttachmentsMultipart);
  329. htmlAndAttachmentsMultipart.addBodyPart(htmlPart);
  330. final String wikiMsgText = getEmailBodyText(event, recipient, unsubToken, "text");
  331. final String htmlMsgText = getEmailBodyText(event, recipient, unsubToken, "html");
  332. textPart.setText(wikiMsgText, "UTF-8");
  333. final AtomicReference<String> rewrittenHtmlMsgText = new AtomicReference<String>(AttachmentLinkRewriter.rewrite(htmlMsgText, new AttachmentLinkRewriter.URLGenerator()
  334. {
  335. /**
  336. * Generate RFC2392 cid: URL from a JIRA attachment relative URL. As a side-effect, adds the attachment
  337. * referenced by the cid: URL to the email being generated.
  338. */
  339. public String generateURL(long attachmentId) throws Exception
  340. {
  341. Attachment att = ComponentManager.getInstance().getAttachmentManager().getAttachment(attachmentId);
  342. File attFile = AttachmentUtils.getAttachmentFile(att);
  343. final MimeBodyPart attachmentPart = new MimeBodyPart();
  344. attachmentPart.setDataHandler(new DataHandler(new FileDataSource(attFile)));
  345. attachmentPart.setFileName(MimeUtility.encodeText(att.getFilename()));
  346. attachmentPart.setHeader("Content-Type", att.getMimetype());
  347. // System.setProperty("mail.mime.encodeparameters", "true");
  348. attachmentPart.setDisposition(Part.ATTACHMENT);
  349. String base_url = applicationProperties.getString(APKeys.JIRA_BASEURL);
  350. String contentID = att.getId() + "@" + base_url;
  351. attachmentPart.setContentID(contentID);
  352. htmlAndAttachmentsMultipart.addBodyPart(attachmentPart);
  353. return "cid:" + contentID;
  354. }
  355. }));
  356. htmlPart.setText(rewrittenHtmlMsgText.get(), "UTF-8", "html");
  357. SingleMailQueueItem item = new SingleMailQueueItem(email);
  358. // We need to save unsubToken to the database, so that later we can look it up as a means of verifying the HTTP
  359. // request of the unsubscribe request
  360. // As a hack, store it in the notificationinstance table by setting the message-ID, then requesting MailThreader
  361. // to store it
  362. email.setMessageId(unsubToken);
  363. MailThreader mailThreader = new JiraMailThreader(event.getEventTypeId(), event.getIssue().getLong("id"));
  364. mailThreader.storeSentEmail(email);
  365. item.setMailThreader(mailThreader);
  366. ManagerFactory.getMailQueue().addItem(item);
  367. } catch (MessagingException e)
  368. {
  369. throw new RuntimeException("Error setting ccmailer email body", e);
  370. }
  371. }
  372. /**
  373. * Return from address ('Joe Bloggs (JIRA)' usually).
  374. *
  375. * @param event issue event
  376. * @return sender's address
  377. */
  378. private String getSenderFrom(IssueEvent event)
  379. {
  380. String from = applicationProperties.getDefaultBackedString(APKeys.EMAIL_FROMHEADER_FORMAT);
  381. if (from == null)
  382. {
  383. return null;
  384. }
  385. final User user = event.getRemoteUser();
  386. String name;
  387. if (user == null)
  388. {
  389. name = "Anonymous";
  390. }
  391. else
  392. {
  393. try
  394. {
  395. String fullName = user.getFullName();
  396. if (org.apache.commons.lang.StringUtils.isBlank(fullName))
  397. {
  398. name = user.getName();
  399. }
  400. else
  401. {
  402. name = fullName;
  403. }
  404. } catch (Exception exception)
  405. {
  406. // this should never fail, but incase it does we don't want to imply it was a anonymous user.
  407. try
  408. {
  409. name = user.getName();
  410. } catch (Exception exception2)
  411. {
  412. name = "";
  413. }
  414. }
  415. }
  416. String email;
  417. try
  418. {
  419. email = (user != null ? user.getEmail() : "");
  420. } catch (Exception exception)
  421. {
  422. email = "";
  423. }
  424. final String hostname = (user != null && email != null ? email.substring(email.indexOf("@") + 1) : "");
  425. from = StringUtils.replaceAll(from, "${fullname}", name);
  426. from = StringUtils.replaceAll(from, "${email}", email);
  427. from = StringUtils.replaceAll(from, "${email.hostname}", hostname);
  428. return from;
  429. }
  430. /**
  431. * Check whether the email recipient should be able to see this event's comment.
  432. * In this implementation, if a comment security level is set, no comment is sent.
  433. *
  434. * @param event
  435. * @param recipient
  436. * @return Whether the recipient has permission to see the triggered event.
  437. */
  438. protected boolean hasPermission(IssueEvent event, InternetAddress recipient)
  439. {
  440. Comment comment = event.getComment();
  441. if (comment != null)
  442. {
  443. String groupLevel = comment.getGroupLevel();
  444. if (groupLevel != null)
  445. {
  446. log.debug("Not emailing " + recipient + " event " + event + " as comment group-level is set, to " + groupLevel);
  447. return false;
  448. }
  449. ProjectRole roleLevel = comment.getRoleLevel();
  450. if (roleLevel != null)
  451. {
  452. log.debug("Not emailing " + recipient + " event " + event + " as comment role-level is set, to " + roleLevel);
  453. return false;
  454. }
  455. }
  456. return true;
  457. }
  458. protected boolean meetsTriggerConditions(IssueEvent event)
  459. {
  460. return isTriggerEvent(event) && eventInTriggeringProject(event) && isWatchersNotified(event);
  461. }
  462. /**
  463. * Checks if the event happened in a project we're configured to care about.
  464. *
  465. * @param event
  466. * @return True if this event affects us.
  467. */
  468. boolean eventInTriggeringProject(IssueEvent event)
  469. {
  470. if (projectKeys.size() > 0 && !projectKeys.contains(event.getIssue().getProjectObject().getKey()))
  471. {
  472. log.debug("Ignoring event, as it is not in allowed project(s) " + projectKeys);
  473. return false;
  474. }
  475. return true;
  476. }
  477. private boolean isTriggerEvent(IssueEvent event)
  478. {
  479. if (triggerEventIDs != null)
  480. {
  481. if (!triggerEventIDs.contains(event.getEventTypeId()))
  482. {
  483. log.debug("Event is not in trigger set " + triggerEventIDs + "; ignoring");
  484. return false;
  485. }
  486. log.debug("Event matches eventIds " + triggerEventIDs);
  487. }
  488. return true;
  489. }
  490. /**
  491. * Checks if the event notifies watchers, and therefore concerns us also.
  492. *
  493. * @param event
  494. * @return True if the event has an All_Watchers notification type.
  495. */
  496. boolean isWatchersNotified(IssueEvent event)
  497. {
  498. boolean eventHasWatchers = false;
  499. GenericValue nsGV = notificationSchemeManager.getNotificationSchemeForProject(event.getIssue().getProject());
  500. if (nsGV != null)
  501. {
  502. try
  503. {
  504. List entities = notificationSchemeManager.getEntities(nsGV, event.getEventTypeId());
  505. if (entities != null)
  506. {
  507. for (GenericValue gv : (List<GenericValue>) entities)
  508. {
  509. if (gv.containsKey("type") && "All_Watchers".equals(gv.getString("type")))
  510. {
  511. eventHasWatchers = true;
  512. break;
  513. }
  514. }
  515. }
  516. } catch (GenericEntityException e)
  517. {
  518. log.error("Unexpected error retrieving entities for notification scheme " + nsGV + ", event " + event);
  519. }
  520. }
  521. else
  522. {
  523. log.debug("Not notifying CC emails, as " + event.getIssue() + " has no notification scheme.");
  524. }
  525. if (!eventHasWatchers)
  526. log.debug("Not notifying CC emails (if any), as " + event.getIssue().getProjectObject() + "'s notification scheme does not notify Watchers on event type " + event.getEventTypeId());
  527. return eventHasWatchers;
  528. }
  529. private boolean recipientAlreadyNotifiedAsWatcher(IssueEvent event, InternetAddress recipient)
  530. {
  531. for (User u : getWatchers(event.getIssue()))
  532. {
  533. if (recipient.equals(u.getEmail()))
  534. {
  535. return true;
  536. }
  537. }
  538. return false;
  539. }
  540. /**
  541. * Given an issue, returns a list of watchers
  542. *
  543. * @param issue The current issue (used to determine who's in role 'watchers').
  544. * @return A list of watchers of type User.
  545. */
  546. private List<User> getWatchers(Issue issue)
  547. {
  548. List<User> myUsers = new ArrayList<User>();
  549. if (issue != null)
  550. {
  551. try
  552. {
  553. myUsers = issueManager.getIssueWatchers(issue);
  554. } catch (Exception e)
  555. {
  556. log.debug("Failed to determine watchers from issue: " + e.toString());
  557. }
  558. }
  559. return myUsers;
  560. }
  561. /**
  562. * Get the CC list for an email
  563. *
  564. * @param issue The current issue
  565. * @return A set of email address Strings.
  566. */
  567. private Set<InternetAddress> getRecipients(@NotNull Issue issue)
  568. {
  569. return new CustomFieldHelperBean(customFieldManager, log).getCustomFieldValueSet(issue, ccCustomField);
  570. }
  571. public boolean isInternal()
  572. {
  573. return false;
  574. }
  575. public boolean isUnique()
  576. {
  577. return false;
  578. }
  579. public String getDescription()
  580. {
  581. return "Sends notifications about issue updates to email addresses in the specified text custom field (the CC field). Parameters are as follows. Note that commas can be escaped with \\." +
  582. " Note that any parameter with a 'default' can be left blank to have that default take effect.<ul>\n" +
  583. "<li><b>" + CC_CUSTOMFIELD_ID + "</b> - The name or ID of the 255 character text custom field you wish to be the 'CC' custom field. Its value should be a list of email addresses separated by semicolons." +
  584. "<li><b>" + PROJECT_KEYS + "</b> - Comma-separated list of project keys (the project key is eg. the 'ABC' in issue key ABC-123). " +
  585. "<li><b>" + TRIGGER_EVENTS + "</b> - comma-separated list of names or IDs of JIRA events to <b>trigger</b> on. Valid events are:" +
  586. "<table border=1>" +
  587. "<tr><th>Event ID</th><th>Event name</th></tr>" +
  588. getEventsTable() +
  589. "</table>" +
  590. "<li><b>" + IGNORED_ME + "</b> - Only trigger if the user performing the action <em>isn't</em> the user being notified. Avoids people being notified on their own changes. Currently this only affects comment notifications, not new issue notifications." +
  591. "</ul>";
  592. }
  593. private String getEventsTable()
  594. {
  595. StringBuffer buf = new StringBuffer();
  596. for (Object o : eventTypeManager.getEventTypes())
  597. {
  598. EventType e = (EventType) o;
  599. buf.append("<tr><td>" + e.getId() + "</td><td>" + e.getName() + "</td></tr>");
  600. }
  601. return buf.toString();
  602. }
  603. /**
  604. * Gets the email body text, either html or plaintext depending on the parameter.
  605. *
  606. * @param event JIRA trigger event.
  607. * @param recipient
  608. * @param unsubscribeToken
  609. * @param text_or_html "text" or "html", indicating the type of mail body the caller wants.
  610. * @return The message text.
  611. */
  612. protected String getEmailBodyText(IssueEvent event, InternetAddress recipient, String unsubscribeToken, final String text_or_html)
  613. {
  614. String result = "";
  615. Map<String, Object> params = new HashMap<String, Object>();
  616. Issue issue = event.getIssue();
  617. params.put("issue", issue);
  618. params.put("event", event);
  619. params.put("comment", event.getComment());
  620. final String commentBody = event.getComment() != null ? event.getComment().getBody() : "";
  621. params.put("htmlCommentBody", rendererManager.getRenderedContent(AtlassianWikiRenderer.RENDERER_TYPE, commentBody, issue.getIssueRenderContext()));
  622. String base_url = applicationProperties.getString(APKeys.JIRA_BASEURL);
  623. params.put("base_url", base_url);
  624. String unsubURL = null;
  625. try
  626. {
  627. unsubURL = base_url + "/secure/UnsubscribeCCField.jspa?cfId=" +
  628. (ccCustomField == null ? "null" : ccCustomField.getIdAsLong()) +
  629. "&unsubToken=" + URLCodec.encode(unsubscribeToken) +
  630. "&email=" + URLCodec.encode(recipient.getAddress());
  631. } catch (UnsupportedEncodingException e1)
  632. {
  633. log.error("Error encoding unsubscribe URL", e1);
  634. }
  635. params.put("unsuburl", unsubURL);
  636. params.put("unsubtoken", unsubscribeToken);
  637. params.put("recipient", recipient);
  638. final String templateFilename = text_or_html.equals("text") ? EMAIL_TEMPLATE_TEXT : EMAIL_TEMPLATE_HTML;
  639. try
  640. {
  641. result = velocityManager.getBody("", templateFilename, params);
  642. } catch (VelocityException e)
  643. {
  644. log.error("Error rendering " + text_or_html + " Cc notification "+templateFilename, e);
  645. result = "(Error rendering email: see server logs for details)";
  646. }
  647. return result;
  648. }
  649. String getCcCustomFieldID()
  650. {
  651. return "" + ccCustomField;
  652. }
  653. }