PageRenderTime 70ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://bitbucket.org/redradish/ccmailer-44x
Java | 1111 lines | 899 code | 76 blank | 136 comment | 173 complexity | 1580af28c10b96003bda188bb161af28 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. package com.redradishtech.jira.ccmailer;
  2. import com.atlassian.core.ofbiz.util.OFBizPropertyUtils;
  3. import com.atlassian.core.user.UserUtils;
  4. import com.atlassian.jira.ComponentManager;
  5. import com.atlassian.jira.config.util.JiraHome;
  6. import com.atlassian.jira.issue.Issue;
  7. import com.atlassian.jira.issue.MutableIssue;
  8. import com.atlassian.jira.issue.comments.Comment;
  9. import com.atlassian.jira.issue.fields.CustomField;
  10. import com.atlassian.jira.issue.search.SearchException;
  11. import com.atlassian.jira.issue.search.SearchProvider;
  12. import com.atlassian.jira.issue.search.SearchRequest;
  13. import com.atlassian.jira.issue.watchers.WatcherManager;
  14. import com.atlassian.jira.jql.parser.DefaultJqlQueryParser;
  15. import com.atlassian.jira.jql.parser.JqlParseException;
  16. import com.atlassian.jira.jql.parser.JqlQueryParser;
  17. import com.atlassian.jira.jql.util.JqlStringSupport;
  18. import com.atlassian.jira.project.ProjectKeys;
  19. import com.atlassian.jira.project.ProjectManager;
  20. import com.atlassian.jira.security.GlobalPermissionManager;
  21. import com.atlassian.jira.security.Permissions;
  22. import com.atlassian.jira.security.roles.ProjectRole;
  23. import com.atlassian.jira.security.roles.ProjectRoleManager;
  24. import com.atlassian.jira.service.util.ServiceUtils;
  25. import com.atlassian.jira.service.util.handler.CreateOrCommentHandler;
  26. import com.atlassian.jira.service.util.handler.RegexCommentHandler;
  27. import com.atlassian.jira.web.bean.PagerFilter;
  28. import com.atlassian.mail.MailException;
  29. import com.atlassian.mail.MailUtils;
  30. import com.atlassian.mail.server.SMTPMailServer;
  31. import com.atlassian.query.Query;
  32. import com.opensymphony.module.propertyset.PropertySet;
  33. import com.opensymphony.user.EntityNotFoundException;
  34. import com.opensymphony.user.Group;
  35. import com.opensymphony.user.User;
  36. import com.redradishtech.jira.ccmailer.util.CustomFieldHelperBean;
  37. import com.atlassian.jira.service.util.handler.redradish.CreateIssueHandler;
  38. import com.atlassian.jira.service.util.handler.redradish.FullCommentHandler;
  39. import com.atlassian.jira.service.util.handler.redradish.NonQuotedCommentHandler;
  40. import org.apache.log4j.Logger;
  41. import org.apache.velocity.exception.VelocityException;
  42. import org.ofbiz.core.entity.GenericValue;
  43. import javax.annotation.Nullable;
  44. import javax.mail.Address;
  45. import javax.mail.Header;
  46. import javax.mail.Message;
  47. import javax.mail.MessagingException;
  48. import javax.mail.internet.AddressException;
  49. import javax.mail.internet.InternetAddress;
  50. import javax.validation.constraints.NotNull;
  51. import java.io.File;
  52. import java.io.FileInputStream;
  53. import java.io.IOException;
  54. import java.text.MessageFormat;
  55. import java.util.*;
  56. import java.util.regex.Matcher;
  57. import java.util.regex.Pattern;
  58. import java.util.regex.PatternSyntaxException;
  59. /**
  60. * A message handler that creates a new issue, or comments on an existing one. Extends the usual {@link CreateOrCommentHandler}
  61. * behaviour in a few ways:
  62. * <ul>
  63. * <li>If 'ccwatcher=true' is set (the default, unlike vanilla CreateOrCommentHandler), Cc'ed users are added to the
  64. * Watcher list for comments, not just for issue-creating emails.
  65. * <li>'cccustomfield' must be specified (or the handler refuses to do anything) and must specify the ID or name of a
  66. * text custom field. This will be populated with email addresses of incoming emails' To: and Cc: fields, providing those emails
  67. * are not already notified via the Watchers list. This behaviour happens irrespective of the 'ccwatcher' setting.
  68. * <li>If an email's sender is unknown and the 'reporterusername' parameter sets the Reporter, the handler puts the full
  69. * email details, including headers, in the issue description.
  70. * <li>'ccassignee' is false by default (ie. the first CC'ed user does not become the assignee).
  71. * </ul>
  72. * There is an optional 'commenttype' field whose default value, 'plaintext', refers to a .vm file on disk. Creating an alternative
  73. * file and setting this parameter lets you customize the comment format (eg. wiki text vs. plaintext).
  74. * See the documentation for other fields.
  75. */
  76. public class CCMailerHandler extends CreateOrCommentHandler
  77. {
  78. private static final Logger log = Logger.getLogger(CCMailerHandler.class);
  79. private static final Object KEY_PARAMFILE = "paramfile";
  80. private static final String KEY_COMMENT_TYPE = "commenttype";
  81. private static final String KEY_CCWATCHER = "ccwatcher";
  82. private static final String KEY_CC_CUSTOMFIELD = "cccustomfield";
  83. private static final String KEY_SPLITREGEX = "splitregex";
  84. private static final String KEY_DONOTWATCHORCC = "donotwatchorcc";
  85. private static final String KEY_VISIBILITYLEVELS = "visibilitylevels";
  86. private static final String KEY_SENDERTOPROJECTMAP = "sendertoprojectmap";
  87. private static final String KEY_BUGZILLA_CUSTOMFIELD = "bugzillafield";
  88. private static final String KEY_BUGZILLA_QUERY = "bugzillaquery";
  89. private static final String KEY_BUGZILLA_REGEX = "bugzillaregex";
  90. /**
  91. * Whether each Cc'ed user should watch the commented issue. Defaults to true. Analogous to {@link com.redradishtech.jira.ccmailer.util.CreateIssueHandler#CC_WATCHER}
  92. */
  93. protected boolean ccWatcher = true;
  94. private String splitRegex;
  95. protected String commentType = "plaintext";
  96. protected String ccCustomField;
  97. static final String SEMICOLON_SPLIT_REGEX = "(?<!\\\\);"; // split on an unescaped semicolon. The regexp is a zero-width negative lookbehind assertion.
  98. static final String COLON_SPLIT_REGEX = "(?<!\\\\):"; // split on an unescaped semicolon. The regexp is a zero-width negative lookbehind assertion.
  99. protected Set<Address> donotWatchOrCc = new HashSet<Address>();
  100. private SortedMap<String, String> visibilityLevels = new TreeMap<String, String>();
  101. private SortedMap<String, String> senderToProjectMappings = new TreeMap<String, String>();
  102. private static final String USER_PROPERTY_KEY = "mailsettings";
  103. protected String bugzillaField;
  104. protected String bugzillaQuery = "{0} ~ {1} order by key desc";
  105. String bugzillaRegex = "Bug (\\d+)";
  106. public void init(Map params)
  107. {
  108. loadParamsFromPropertiesFile(params);
  109. log.debug("CCMailerHandler.init(params: " + params + ")");
  110. super.init(params);
  111. if (params.containsKey(KEY_CCWATCHER))
  112. {
  113. ccWatcher = Boolean.valueOf((String) params.get(KEY_CCWATCHER));
  114. }
  115. if (params.containsKey(KEY_SPLITREGEX))
  116. {
  117. splitRegex = ((String) params.get(KEY_SPLITREGEX));
  118. }
  119. if (params.containsKey(KEY_DONOTWATCHORCC))
  120. {
  121. String[] bannedEmailStrs = ((String) params.get(KEY_DONOTWATCHORCC)).split(SEMICOLON_SPLIT_REGEX);
  122. for (String emailStr : bannedEmailStrs)
  123. {
  124. try
  125. {
  126. donotWatchOrCc.add(new InternetAddress(emailStr));
  127. } catch (AddressException e)
  128. {
  129. log.error("Ignoring invalid " + KEY_DONOTWATCHORCC + " parameter for CCMailerHandler listener: " + e, e);
  130. }
  131. }
  132. }
  133. if (params.containsKey(KEY_COMMENT_TYPE))
  134. {
  135. commentType = (String) params.get(KEY_COMMENT_TYPE);
  136. }
  137. if (params.containsKey(KEY_CC_CUSTOMFIELD))
  138. {
  139. ccCustomField = (String) params.get(KEY_CC_CUSTOMFIELD);
  140. }
  141. if (params.containsKey(KEY_BUGZILLA_CUSTOMFIELD))
  142. {
  143. bugzillaField = (String) params.get(KEY_BUGZILLA_CUSTOMFIELD);
  144. }
  145. if (params.containsKey(KEY_BUGZILLA_QUERY))
  146. {
  147. bugzillaQuery = (String) params.get(KEY_BUGZILLA_QUERY);
  148. }
  149. if (params.containsKey(KEY_BUGZILLA_REGEX))
  150. {
  151. bugzillaRegex = (String) params.get(KEY_BUGZILLA_REGEX);
  152. }
  153. if (params.containsKey(KEY_VISIBILITYLEVELS) && params.get(KEY_VISIBILITYLEVELS) != null)
  154. {
  155. String[] vlStrs = ((String) params.get(KEY_VISIBILITYLEVELS)).split(SEMICOLON_SPLIT_REGEX);
  156. for (String vlStr : vlStrs)
  157. {
  158. vlStr = vlStr.replaceAll("\\\\\\;", ";"); // unescape any escaped delimiters
  159. String[] keyValPair = vlStr.split(COLON_SPLIT_REGEX);
  160. if (keyValPair.length != 2)
  161. {
  162. throw new RuntimeException("CCMailerHandler " + KEY_VISIBILITYLEVELS + " parameter has invalid format. Segment '" + vlStr + "' was expected to contain ':' separating key and value.");
  163. }
  164. keyValPair[0] = keyValPair[0].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
  165. keyValPair[1] = keyValPair[1].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
  166. visibilityLevels.put(keyValPair[0], keyValPair[1]);
  167. }
  168. }
  169. if (params.containsKey(KEY_SENDERTOPROJECTMAP) && params.get(KEY_SENDERTOPROJECTMAP) != null)
  170. {
  171. String[] upStrs = ((String) params.get(KEY_SENDERTOPROJECTMAP)).split(SEMICOLON_SPLIT_REGEX);
  172. for (String upStr : upStrs)
  173. {
  174. upStr = upStr.replaceAll("\\\\\\;", ";"); // unescape any escaped delimiters
  175. String[] keyValPair = upStr.split(COLON_SPLIT_REGEX);
  176. if (keyValPair.length != 2)
  177. {
  178. throw new RuntimeException("CCMailerHandler " + KEY_SENDERTOPROJECTMAP + " parameter has invalid format. Segment '" + upStr + "' was expected to contain ':' separating key and value.");
  179. }
  180. keyValPair[0] = keyValPair[0].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
  181. keyValPair[1] = keyValPair[1].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
  182. senderToProjectMappings.put(keyValPair[0], keyValPair[1]);
  183. }
  184. }
  185. }
  186. private void loadParamsFromPropertiesFile(Map params)
  187. {
  188. if (params.containsKey(KEY_PARAMFILE))
  189. {
  190. String paramFilename = (String) params.get(KEY_PARAMFILE);
  191. Properties props = new Properties();
  192. try
  193. {
  194. JiraHome jiraHome = ComponentManager.getComponentInstanceOfType(JiraHome.class);
  195. File propFile = new File(jiraHome.getHome(), paramFilename);
  196. props.load(new FileInputStream(propFile));
  197. log.debug("Loading " + this.getClass().getName() + " service properties from " + props);
  198. Enumeration e = props.keys();
  199. while (e.hasMoreElements())
  200. {
  201. String propName = (String) e.nextElement();
  202. if (!params.containsKey(propName))
  203. {
  204. params.put(propName, props.get(propName));
  205. }
  206. }
  207. } catch (IOException e)
  208. {
  209. throw new RuntimeException("Error reading file " + params.get(KEY_PARAMFILE) + ", specified as " + KEY_PARAMFILE + " parameter for a " + getClass().getName() + " email handler. " + e, e);
  210. }
  211. }
  212. }
  213. // Main logic. The basic structure is the same as CreateOrCommentHandler, overriding called classes to render the full
  214. // email in the body, setting the Watchers on comments and setting CC for new issues and new comments.
  215. public boolean handleMessage(Message message) throws MessagingException
  216. {
  217. if (!canHandleMessage(message))
  218. {
  219. return deleteEmail;
  220. }
  221. GenericValue issueGV = getAssociatedIssue(message);
  222. // We found an issue for this email?
  223. if (issueGV != null)
  224. {
  225. boolean doDelete = false;
  226. //add the message as a comment to the issueGV
  227. final Map vl = visibilityLevels;
  228. if (splitRegex != null)
  229. {
  230. log.debug("Creating comment from email with RegexCommentHandler (regex: " + splitRegex + ")");
  231. RegexCommentHandler rc = new RegexCommentHandler()
  232. {
  233. protected String getEmailBody(Message message) throws MessagingException
  234. {
  235. User reporter = findReporter(message);
  236. // Record full email headers if From: address is not known to JIRA
  237. log.debug("Splitting on regex " + splitRegex + " body:\n" + MailUtils.getBody(message));
  238. if (reporter != null) return super.getEmailBody(message);
  239. else return getCommentBody(message, super.getEmailBody(message));
  240. }
  241. protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
  242. {
  243. final String groupOrRoleName = (String) vl.get(commenterUsername);
  244. String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
  245. Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
  246. return commentManager.create(issue, commenterUsername, body, group, role, false);
  247. }
  248. @Override
  249. protected GenericValue getAssociatedIssue(Message message)
  250. {
  251. return myGetAssociatedIssue(message);
  252. }
  253. };
  254. rc.setErrorHandler(this.getErrorHandler());
  255. rc.init(params);
  256. doDelete = rc.handleMessage(message);
  257. }
  258. else if (stripquotes != null && "true".equalsIgnoreCase(stripquotes)) //if stripquotes not defined in setup
  259. {
  260. log.debug("Creating comment from email with NonQuotedCommentHandler");
  261. NonQuotedCommentHandler nq = new NonQuotedCommentHandler()
  262. {
  263. protected String getEmailBody(Message message) throws MessagingException
  264. {
  265. User reporter = findReporter(message);
  266. // Record full email headers if From: address is not known to JIRA
  267. if (reporter != null) return super.getEmailBody(message);
  268. else return getCommentBody(message, super.getEmailBody(message));
  269. }
  270. protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
  271. {
  272. String groupOrRoleName = (String) vl.get(commenterUsername);
  273. String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
  274. Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
  275. return commentManager.create(issue, commenterUsername, body, group, role, false);
  276. }
  277. @Override
  278. protected GenericValue getAssociatedIssue(Message message)
  279. {
  280. return myGetAssociatedIssue(message);
  281. }
  282. };
  283. nq.setErrorHandler(this.getErrorHandler());
  284. nq.init(params);
  285. doDelete = nq.handleMessage(message); //get message without quotes
  286. }
  287. else
  288. {
  289. FullCommentHandler fc = new FullCommentHandler()
  290. {
  291. protected String getEmailBody(Message message) throws MessagingException
  292. {
  293. User reporter = findReporter(message);
  294. // Record full email headers if From: address is not known to JIRA
  295. if (reporter != null) return super.getEmailBody(message);
  296. else return getCommentBody(message, super.getEmailBody(message));
  297. }
  298. protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
  299. {
  300. String groupOrRoleName = (String) vl.get(commenterUsername);
  301. String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
  302. Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
  303. return commentManager.create(issue, commenterUsername, body, group, role, false);
  304. }
  305. @Override
  306. protected GenericValue getAssociatedIssue(Message message)
  307. {
  308. return myGetAssociatedIssue(message);
  309. }
  310. };
  311. fc.setErrorHandler(this.getErrorHandler());
  312. fc.init(params);
  313. doDelete = fc.handleMessage(message); //get message with quotes
  314. }
  315. // If the issueGV exists and certain emails were CC'ed, add these emails to the watcher/CC fields *after*
  316. // adding the comment, so that they do not get two emails (one from the CC, one from JIRA).
  317. if (ccWatcher)
  318. {
  319. myAddCcWatchersToIssue(message, issueGV);
  320. }
  321. Issue issueObj = ComponentManager.getInstance().getIssueFactory().getIssue(issueGV);
  322. // Populate the CC custom field with any email addresses not already a watcher
  323. populateCCField(message, issueGV, issueObj.getReporter());
  324. return doDelete;
  325. }
  326. else
  327. { // issueGV == null, so create new issueGV in default project
  328. CreateIssueHandler createIssueHandler = new CreateIssueHandler()
  329. {
  330. public void init(Map params)
  331. {
  332. // Default ccassignee to false, but let this be overridden in params if necessary.
  333. ccAssignee = false;
  334. super.init(params);
  335. }
  336. /**
  337. * Populate the issueGV summary with full email header details, if it was reported by someone not known
  338. * to JIRA. Otherwise, create the usual issueGV summary from the message body.
  339. */
  340. protected String getDescription(User reporter, Message message) throws MessagingException
  341. {
  342. if (reporteruserName != null && reporteruserName.equals(reporter.getName()))
  343. {
  344. // From: address doesn't match JIRA user; record full email details in description
  345. return getCommentBody(message, MailUtils.getBody(message));
  346. }
  347. else
  348. {
  349. // From: address matched someone; get standard description
  350. return super.getDescription(reporter, message);
  351. }
  352. }
  353. @Override
  354. protected GenericValue getProject(Message message)
  355. {
  356. User reporter = null;
  357. List senders = null;
  358. try
  359. {
  360. reporter = findReporter(message);
  361. senders = MailUtils.getSenders(message);
  362. } catch (MessagingException e)
  363. {
  364. log.error("Error parsing From: address of email " + message + " while processing " + KEY_SENDERTOPROJECTMAP + " param on CCMailerHandler. Default project " + projectKey + " will be used. " + e, e);
  365. }
  366. ProjectManager projectManager = ComponentManager.getInstance().getProjectManager();
  367. String newProjectKey = null;
  368. // First check if their is a user property explicitly requiring emails from this reporter to be in a certain project
  369. newProjectKey = getProjectKeyFromUserProperty(reporter);
  370. if (newProjectKey != null)
  371. {
  372. log.debug("Associated user " + reporter + " with project " + newProjectKey + " via user property");
  373. }
  374. else if (senders != null && senderToProjectMappings != null)
  375. {
  376. // Keys here will be strings like {group}jira-developers or {email}.*@localhost
  377. for (String groupOrEmailStr : senderToProjectMappings.keySet())
  378. {
  379. String mappedProjectKey = senderToProjectMappings.get(groupOrEmailStr);
  380. if (projectManager.getProjectObjByKey(mappedProjectKey) == null)
  381. {
  382. log.warn("Mapping " + groupOrEmailStr + ":" + mappedProjectKey + " in " + KEY_SENDERTOPROJECTMAP + " param of CCMailerListneer is invalid; project '" +
  383. mappedProjectKey + "' does not exist. Using default project " + projectKey);
  384. continue;
  385. }
  386. String group = getCommentGroup(groupOrEmailStr, KEY_SENDERTOPROJECTMAP);
  387. if (reporter != null && reporter.inGroup(group))
  388. {
  389. log.debug("Email sender " + reporter + " in group " + group);
  390. if (newProjectKey != null)
  391. {
  392. log.warn("Two items in " + KEY_SENDERTOPROJECTMAP + " matched user " + reporter + ". Giving up and using default project " + projectKey);
  393. newProjectKey = null;
  394. break;
  395. }
  396. else
  397. newProjectKey = mappedProjectKey;
  398. }
  399. Pattern p = getCommentEmailRegex(groupOrEmailStr, KEY_SENDERTOPROJECTMAP);
  400. if (p != null)
  401. {
  402. for (Object senderEmail : senders)
  403. {
  404. Matcher m = p.matcher("" + senderEmail);
  405. if (m.find())
  406. {
  407. log.debug("Email sender's email " + senderEmail + " matched regex " + p);
  408. if (newProjectKey != null)
  409. {
  410. log.warn("Two items in " + KEY_SENDERTOPROJECTMAP + " matched user " + reporter + ". Giving up and using default project " + projectKey);
  411. newProjectKey = null;
  412. break;
  413. }
  414. else
  415. newProjectKey = mappedProjectKey;
  416. }
  417. else
  418. {
  419. log.debug("Email '" + senderEmail + "' does not match specified regex " + p);
  420. }
  421. }
  422. }
  423. }
  424. if (newProjectKey != null)
  425. {
  426. log.debug("Email sender " + reporter + " had " + KEY_SENDERTOPROJECTMAP + " mapping; using mapped project key " + newProjectKey);
  427. }
  428. }
  429. if (newProjectKey != null)
  430. {
  431. projectKey = newProjectKey;
  432. return projectManager.getProjectByKey(newProjectKey);
  433. }
  434. else
  435. {
  436. return super.getProject(message);
  437. }
  438. }
  439. /**
  440. * Override the point at which users are added to Watchers to also add emails to the CC field.
  441. * Note that by this point the issue has already been created, and all events fired, so neither new watchers
  442. * not CC emails get notified of the event.
  443. */
  444. public void addCcWatchersToIssue(Message message, GenericValue issueGV, User reporter) throws MessagingException
  445. {
  446. // Any recognised emails become watchers
  447. myAddCcWatchersToIssue(message, issueGV);
  448. populateCCField(message, issueGV, reporter);
  449. }
  450. };
  451. createIssueHandler.setErrorHandler(this.getErrorHandler());
  452. createIssueHandler.init(params);
  453. return createIssueHandler.handleMessage(message);
  454. }
  455. }
  456. @Override
  457. protected GenericValue getAssociatedIssue(Message message)
  458. {
  459. return myGetAssociatedIssue(message);
  460. }
  461. /**
  462. * Determines which issue is referred to by the given email.
  463. *
  464. * @param message Email
  465. * @return Associated issue, or null of none associated.
  466. */
  467. public GenericValue myGetAssociatedIssue(@NotNull Message message)
  468. {
  469. String subject = null;
  470. try
  471. {
  472. subject = message.getSubject();
  473. } catch (MessagingException e)
  474. {
  475. log.error(e, e);
  476. return null;
  477. }
  478. GenericValue issueGV = ServiceUtils.findIssueInString(subject); // Explicit key in subject?
  479. if (issueGV == null) issueGV = findIssueFromBugzillaIdInString(subject); // Otherwise is there a Bugzilla ID?
  480. if (issueGV == null) issueGV = super.getAssociatedIssue(message); // Otherwise does In-Reply-To: match?
  481. return issueGV;
  482. }
  483. /**
  484. * Returns a project associated with the user via a JIRA user property.
  485. *
  486. * @param user The user to look up (possibly null)
  487. * @return A valid JIRA project key if one is associated with this user, otherwise null.
  488. */
  489. private
  490. @Nullable
  491. String getProjectKeyFromUserProperty(@Nullable User user)
  492. {
  493. if (user == null) return null;
  494. PropertySet propertySet = user.getPropertySet();
  495. String actualPropertyKeyName = null;
  496. for (Object key : propertySet.getKeys("jira.meta."))
  497. {
  498. if (key instanceof String && ("jira.meta." + USER_PROPERTY_KEY).equalsIgnoreCase((String) key))
  499. {
  500. actualPropertyKeyName = (String) key;
  501. break;
  502. }
  503. }
  504. if (actualPropertyKeyName != null && propertySet.exists(actualPropertyKeyName))
  505. { // exists() should be redundant
  506. String keyValue = propertySet.getString(actualPropertyKeyName);
  507. String[] keyValPair = keyValue.split("(?<!\\\\)="); // unescaped equal sign
  508. if (keyValPair.length != 2)
  509. {
  510. log.error("CCMailerHandler user " + user + "'s property " + actualPropertyKeyName + "' has an invalid format. Segment '" + keyValue + "' was expected to contain '=' separating key and value.");
  511. return null;
  512. }
  513. if (!"project".equals(keyValPair[0]))
  514. {
  515. log.error("Unexpected property key " + keyValPair[0] + " in user " + user + " property " + actualPropertyKeyName);
  516. return null;
  517. }
  518. ProjectManager projectManager = ComponentManager.getInstance().getProjectManager();
  519. String projectKey = keyValPair[1];
  520. if (projectManager.getProjectObjByKey(projectKey) == null)
  521. {
  522. log.warn("User " + user + " has JIRA property '" + actualPropertyKeyName + "' set specifying which project to create issues in when email from this user's address is received, however the property's value '" + keyValue +
  523. "' does not specify a valid project. Ignoring the user property.");
  524. }
  525. else return projectKey;
  526. }
  527. return null;
  528. }
  529. private Pattern getCommentEmailRegex(String groupOrEmailName, String paramName)
  530. {
  531. if (groupOrEmailName != null)
  532. if (groupOrEmailName.startsWith("{email}"))
  533. {
  534. String emailRegexStr = groupOrEmailName.substring("{email}".length());
  535. try
  536. {
  537. return Pattern.compile(emailRegexStr);
  538. } catch (PatternSyntaxException e)
  539. {
  540. log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies invalid regex " + emailRegexStr + "\n" + e, e);
  541. return null;
  542. }
  543. }
  544. return null;
  545. }
  546. /**
  547. * Given a group/role specifier from visibilitylevels param, return the role ID if a role is referred to, or null otherwise.
  548. *
  549. * @param groupOrRoleName Eg. '{role}Users' or 'jira-developers' (a group hence returns null), or null
  550. * @param paramName Parameter the groupOrRoleName came from, for logging/debugging purposes.
  551. * @return Group id, eg. 10000 for Users
  552. */
  553. @Nullable
  554. Long getCommentRole(@Nullable String groupOrRoleName, String paramName)
  555. {
  556. if (groupOrRoleName != null)
  557. if (groupOrRoleName.startsWith("{role}"))
  558. {
  559. String roleName = groupOrRoleName.substring("{role}".length());
  560. ProjectRole role = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class).getProjectRole(roleName);
  561. if (role == null)
  562. {
  563. log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies nonexistent role " + roleName);
  564. }
  565. else
  566. {
  567. return role.getId();
  568. }
  569. }
  570. return null;
  571. }
  572. /**
  573. * Given a group/role specifier from visibilitylevels param, returns the group name if a group is referred to, or null otherwise.
  574. *
  575. * @param groupOrRoleName Eg. 'jira-developers', '{group}jira-developers' (equivalent), or a role name or null.
  576. * @param paramName Name of the handler parameter that contained groupOrRoleName; used only for logging.
  577. * @return Group string, eg. 'jira-developers' if it is valid, null otherwise.
  578. */
  579. @Nullable
  580. String getCommentGroup(@Nullable String groupOrRoleName, final String paramName)
  581. {
  582. if (groupOrRoleName != null)
  583. if (groupOrRoleName.startsWith("{group}") || !groupOrRoleName.startsWith("{"))
  584. {
  585. String groupName = groupOrRoleName.startsWith("{group}") ? groupOrRoleName.substring("{group}".length()) : groupOrRoleName;
  586. Group group = ComponentManager.getInstance().getUserUtil().getGroup(groupName);
  587. if (group == null)
  588. {
  589. log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies nonexistent group " + groupName);
  590. }
  591. else
  592. {
  593. return groupName;
  594. }
  595. }
  596. return null;
  597. }
  598. /**
  599. * If the given email subject contains a Bugzilla reference (eg. 'DO NOT REPLY (Bug 12345) Blah blah..') then return the JIRA issue
  600. * having a Bugzilla custom field with the referenced value (eg. 12345)
  601. *
  602. * @param subject Email subject
  603. * @return Associated issue, or null if none found or lookup failed.`
  604. */
  605. private GenericValue findIssueFromBugzillaIdInString(String subject)
  606. {
  607. if (bugzillaField == null || bugzillaQuery == null || bugzillaRegex == null) return null;
  608. if (subject == null)
  609. {
  610. log.warn("Email has null subject; not associating with any issue.");
  611. return null;
  612. }
  613. try
  614. {
  615. Pattern p;
  616. try
  617. {
  618. p = Pattern.compile(bugzillaRegex);
  619. } catch (PatternSyntaxException e)
  620. {
  621. log.error("Invalid " + KEY_BUGZILLA_REGEX + " parameter in " + this.getClass().getName() + " service instance. Parameter value '" + bugzillaRegex + "' is not a valid regular expression: " + e, e);
  622. return null;
  623. }
  624. Matcher m = p.matcher(subject);
  625. if (m.find())
  626. {
  627. String bugIdStr = m.group(1);
  628. log.debug("Found reference to Bugzilla ticket " + bugIdStr + ". Searching custom field '" + bugzillaField + "' for it..");
  629. JqlStringSupport supp = (JqlStringSupport) ComponentManager.getInstance().getContainer().getComponentInstanceOfType(JqlStringSupport.class);
  630. JqlQueryParser jqlParser = new DefaultJqlQueryParser();
  631. String jqlQueryStr = null;
  632. try
  633. {
  634. jqlQueryStr = MessageFormat.format(bugzillaQuery, supp.encodeFieldName(bugzillaField), supp.encodeStringValue(bugIdStr));
  635. } catch (IllegalArgumentException e)
  636. {
  637. log.error("Error parsing " + KEY_BUGZILLA_QUERY + " parameter. Parameter value '" + bugzillaQuery + "' must be a java MessageFormat() string containing {0} for the bugzilla custom field name and {1} for the value. " + e, e);
  638. return null;
  639. }
  640. log.debug("Searching for issue with Bugzilla ID: running JQL query: " + jqlQueryStr);
  641. Query jqlQuery = null;
  642. try
  643. {
  644. jqlQuery = jqlParser.parseQuery(jqlQueryStr);
  645. } catch (JqlParseException e)
  646. {
  647. log.error("Unexpected error parsing JQL '" + jqlQueryStr + "', constructed from " + KEY_BUGZILLA_QUERY + " parameter '" + bugzillaQuery + "': " + e, e);
  648. return null;
  649. }
  650. SearchRequest sr = new SearchRequest(jqlQuery);
  651. // SearchRequest sr = new SearchRequest(cb.buildQuery());
  652. // return validateAndSearch(user, searchRequest, pagerFilter);
  653. SearchProvider searcher = ComponentManager.getInstance().getSearchProvider();
  654. try
  655. {
  656. List<Issue> issues = searcher.searchOverrideSecurity(sr.getQuery(), getAnyAdminUser(), PagerFilter.getUnlimitedFilter(), null).getIssues();
  657. if (issues == null || issues.size() == 0)
  658. {
  659. log.debug("No issues found with " + bugzillaField + " cf value " + bugIdStr);
  660. }
  661. Issue i = issues.get(0);
  662. if (issues.size() > 1)
  663. {
  664. StringBuffer buf = new StringBuffer();
  665. for (Issue ii : issues) buf.append(ii.getKey()).append(" ");
  666. log.warn("Found more than 1 (" + issues.size() + ") issues matching " + jqlQueryStr + " ( " + buf + "). Associating comment with first (" + i.getKey() + ")");
  667. }
  668. log.debug("Issue " + i.getKey() + " matches " + jqlQueryStr);
  669. return ServiceUtils.getIssue(i.getKey());
  670. } catch (SearchException e)
  671. {
  672. log.error(e, e);
  673. } catch (EntityNotFoundException e)
  674. {
  675. log.error(e, e);
  676. }
  677. }
  678. } catch (Exception e)
  679. {
  680. log.error("Error extracting bugzilla ID from subject: " + e, e);
  681. return null;
  682. }
  683. return null;
  684. }
  685. // Thanks Jamie Echlin http://forums.atlassian.com/thread.jspa?messageID=257334242&#257334242
  686. public User getAnyAdminUser() throws EntityNotFoundException
  687. {
  688. GlobalPermissionManager globalPermissionManager = (GlobalPermissionManager) ComponentManager.getInstance().getComponentInstanceOfType(GlobalPermissionManager.class);
  689. Collection groups = globalPermissionManager.getGroups(Permissions.ADMINISTER);
  690. for (Iterator iterator = groups.iterator(); iterator.hasNext(); )
  691. {
  692. Group group = (Group) iterator.next();
  693. for (Iterator userIterator = group.getUsers().iterator(); userIterator.hasNext(); )
  694. {
  695. String userName = (String) userIterator.next();
  696. return UserUtils.getUser(userName);
  697. }
  698. }
  699. return null;
  700. }
  701. /**
  702. * Populate the CC custom field with any email addresses not already a watcher, reporter or default JIRA address.
  703. */
  704. private void populateCCField(Message message, GenericValue issueGV, User reporter)
  705. {
  706. try
  707. {
  708. if (ccCustomField != null)
  709. {
  710. MutableIssue issue = ComponentManager.getInstance().getIssueFactory().getIssue(issueGV);
  711. Set<Address> unnotifiedEmails = findEmailsToCC(message, issue, reporter);
  712. CustomFieldHelperBean cfHelper = new CustomFieldHelperBean(ComponentManager.getInstance().getCustomFieldManager(), log);
  713. CustomField cf = cfHelper.lookupCustomField(ccCustomField);
  714. if (cf != null)
  715. {
  716. Set<InternetAddress> alreadyCCedEmails = cfHelper.getCustomFieldValueSet(issue, cf);
  717. unnotifiedEmails.removeAll(alreadyCCedEmails);
  718. try
  719. {
  720. cfHelper.addCustomFieldValueSet(issue, cf, unnotifiedEmails);
  721. for (Address email : unnotifiedEmails)
  722. {
  723. log.info("Added to " + issue.getKey() + " " + ccCustomField + " list: " + email);
  724. }
  725. } catch (Exception e)
  726. {
  727. log.error("Error setting custom field value " + unnotifiedEmails + " for cf " + cf + " to issue " + issue + ". Not setting CC field. Error is: " + e, e);
  728. }
  729. }
  730. }
  731. else
  732. {
  733. log.warn("No cccustomfield parameter specified for a CCMailerHandler");
  734. }
  735. } catch (Throwable t)
  736. {
  737. log.error("Error populating CC field: " + t, t);
  738. }
  739. }
  740. /**
  741. * Don't handle the message unless the cccustomfield parameter has been specified.
  742. *
  743. * @param message message to check if it can be handled
  744. * @return If we should attempt to handle this email.
  745. */
  746. protected boolean canHandleMessage(Message message)
  747. {
  748. if (ccCustomField == null)
  749. {
  750. log.error("CCMailerHandler specified, but not configured with a cccustomfield parameter. Not handling the email.");
  751. return false;
  752. }
  753. return super.canHandleMessage(message);
  754. }
  755. /**
  756. * Searches an email's To, Cc & similar fields, returning those that aren't yet (but could be) notified of issue updates,
  757. * and who aren't existing users in JIRA. This means excluding watchers, the default JIRA From: address (eg. jira@example.com) and catchEmail address.
  758. * The intention is to return emails of 'customers' sending email to the system, who don't have a JIRA account and who don't want one.
  759. */
  760. private Set<Address> findEmailsToCC(Message message, MutableIssue issue, User reporter) throws MessagingException
  761. {
  762. // Start by assuming all To: Cc: etc. fields want to receive future updates, and should be added to the CC field
  763. Set<Address> unnotifiedEmails = new HashSet<Address>();
  764. for (Address a : message.getAllRecipients())
  765. {
  766. unnotifiedEmails.add(a);
  767. }
  768. // The email sender presumably is interested and wants future updates. If this email address matches 'reporter'
  769. // username address it will be removed later
  770. unnotifiedEmails.addAll(getAllNonUserEmails(message.getFrom()));
  771. // Don't CC people who are already Watchers
  772. final WatcherManager watchermanager = ComponentManager.getInstance().getWatcherManager();
  773. Collection<User> currentNotifiedUserList = watchermanager.getCurrentWatchList(Locale.getDefault(), issue.getGenericValue());
  774. for (User user : currentNotifiedUserList)
  775. {
  776. unnotifiedEmails.remove(new InternetAddress(user.getEmail()));
  777. }
  778. // Don't CC the official issue reporter - if they want notifications they'll add this to the notification scheme
  779. if (reporter != null) unnotifiedEmails.remove(new InternetAddress(reporter.getEmail()));
  780. // Remove the JIRA address, eg. jira@example.com, otherwise we'll be creating a mail loop.
  781. Address fromEmailAddress = getJIRAFromAddress(issue);
  782. if (fromEmailAddress != null && unnotifiedEmails.contains(fromEmailAddress))
  783. {
  784. log.debug("Not adding " + fromEmailAddress + " to CC list as it is this project's default From address");
  785. unnotifiedEmails.remove(fromEmailAddress);
  786. }
  787. // Remove the JIRA address, indicated as catchemail=... in the handler params.
  788. if (catchEmail != null)
  789. {
  790. try
  791. {
  792. InternetAddress catchEmailAddr = new InternetAddress(catchEmail);
  793. if (unnotifiedEmails.contains(catchEmailAddr))
  794. {
  795. log.debug("Not adding " + catchEmail + " to CC list as it is this handler's catchEmail value");
  796. unnotifiedEmails.remove(catchEmailAddr);
  797. }
  798. } catch (AddressException e)
  799. {
  800. log.error("Invalid 'catchemail' address; not stripping it from CC list for " + issue);
  801. }
  802. }
  803. // Remove banned email addresses
  804. unnotifiedEmails.removeAll(donotWatchOrCc);
  805. return unnotifiedEmails;
  806. }
  807. protected void recordMessageId(String type, Message message, Long issueId) throws MessagingException
  808. {
  809. super.recordMessageId(type, message, issueId);
  810. }
  811. /**
  812. * Return the email address that JIRA mails generated for the specified issue will come from.
  813. * For example, JIRA emails might come from jira@example.com.
  814. *
  815. * @param issue Issue
  816. * @return Email address, or null if a valid email address is not set.
  817. */
  818. private InternetAddress getJIRAFromAddress(MutableIssue issue)
  819. {
  820. ComponentManager componentManager = ComponentManager.getInstance();
  821. ProjectManager projectManager = componentManager.getProjectManager();
  822. GenericValue project = projectManager.getProject(issue.getGenericValue());
  823. if (project == null)
  824. {
  825. throw new IllegalArgumentException("Project not found for issue " + issue);
  826. }
  827. PropertySet projectPS = OFBizPropertyUtils.getPropertySet(project);
  828. String fromAddressStr = projectPS.getString(ProjectKeys.EMAIL_SENDER);
  829. // If there is no project-specific From address, look for the default.
  830. if (fromAddressStr == null)
  831. {
  832. final SMTPMailServer defaultSMTPMailServer;
  833. try
  834. {
  835. defaultSMTPMailServer = componentManager.getMailServerManager().getDefaultSMTPMailServer();
  836. } catch (MailException e)
  837. {
  838. log.error("Error looking up default SMTP server. Ignoring", e);
  839. return null;
  840. }
  841. if (defaultSMTPMailServer != null)
  842. fromAddressStr = defaultSMTPMailServer.getDefaultFrom();
  843. }
  844. InternetAddress fromAddress = null;
  845. try
  846. {
  847. fromAddress = new InternetAddress(fromAddressStr);
  848. } catch (AddressException e)
  849. {
  850. log.error("The email address '" + fromAddressStr + " used as 'From' address for " + issue + " issues is invalid", e);
  851. }
  852. return fromAddress;
  853. }
  854. /**
  855. * Renders an email's text in a JIRA comment. By default this means showing all the email headers, then the email body.
  856. *
  857. * @param message email
  858. * @param emailBody Email body text in printable form (from superclass usually).
  859. * @return Comment body string.
  860. * @throws MessagingException If there was some error extracting the email contents.
  861. */
  862. protected String getCommentBody(Message message, String emailBody) throws MessagingException
  863. {
  864. String result = null;
  865. Map<String, Object> params = new HashMap<String, Object>();
  866. Enumeration allHeaders = message.getAllHeaders();
  867. HashMap<String, String> headers = new HashMap<String, String>();
  868. while (allHeaders.hasMoreElements())
  869. {
  870. Header header = (Header) allHeaders.nextElement();
  871. headers.put(header.getName(), header.getValue());
  872. }
  873. params.put("subject", message.getSubject());
  874. params.put("headers", headers);
  875. params.put("cc", message.getHeader("cc"));
  876. params.put("from", message.getFrom());
  877. params.put("body", emailBody);
  878. try
  879. {
  880. result = ComponentManager.getInstance().getVelocityManager().getBody("", "templates/ccmailer/comment/" + commentType + ".vm", params);
  881. } catch (VelocityException e)
  882. {
  883. log.error("Error rendering Cc notification", e);
  884. }
  885. return result;
  886. }
  887. /**
  888. * Adds all valid users that are in the email To and cc fields as watchers of the issue.
  889. * This method is identical to {@link com.atlassian.jira.service.util.handler.redradish.CreateIssueHandler#addCcWatchersToIssue(javax.mail.Message,org.ofbiz.core.entity.GenericValue,com.opensymphony.user.User)}.
  890. * Reimplemented here to allow the behaviour for new comments, not just new issues.
  891. *
  892. * @param message message to extract the email addresses from
  893. * @param issue issue to add the watchers to
  894. * @throws MessagingException message errors
  895. */
  896. public void myAddCcWatchersToIssue(Message message, GenericValue issue)
  897. {
  898. try
  899. {
  900. Collection<User> users = getAllUsersFromEmails(message.getAllRecipients());
  901. User emailSender = getReporter(message);
  902. try
  903. {
  904. if (!emailSender.equals(UserUtils.getUser(reporteruserName)))
  905. {
  906. users.add(emailSender);
  907. }
  908. } catch (EntityNotFoundException e)
  909. {
  910. log.warn("Nonexistent reporterusername user: " + reporteruserName);
  911. }
  912. users = removeUserMatchingEmails(users, donotWatchOrCc, "Not watching %s as it is in the " + KEY_DONOTWATCHORCC + " parameter.");
  913. if (!users.isEmpty())
  914. {
  915. final WatcherManager watchermanager = ComponentManager.getInstance().getWatcherManager();
  916. for (User user : users)
  917. {
  918. watchermanager.startWatching(user, issue);
  919. log.debug("Email address " + user.getEmail() + " added as watcher");
  920. }
  921. }
  922. } catch (Throwable t)
  923. {
  924. log.error("Error adding CC'ed JIRA users to watchers: " + t, t);
  925. }
  926. }
  927. private Collection<User> removeUserMatchingEmails(final Collection<User> users, Set<Address> donotWatchOrCc, String debugMsg) throws AddressException
  928. {
  929. List<User> newUsers = new ArrayList<User>();
  930. for (User u : users)
  931. {
  932. InternetAddress addr = new InternetAddress(u.getEmail());
  933. if (!donotWatchOrCc.contains(addr))
  934. {
  935. newUsers.add(u);
  936. }
  937. else
  938. log.debug(String.format(debugMsg, u));
  939. }
  940. return newUsers;
  941. }
  942. private Collection<User> getAllUsersFromEmails(Address addresses[])
  943. {
  944. return getAllUsersFromEmails(Arrays.asList(addresses));
  945. }
  946. private List<User> getAllUsersFromEmails(Collection<Address> addresses)
  947. {
  948. if (addresses == null || addresses.size() == 0)
  949. {
  950. return Collections.EMPTY_LIST;
  951. }
  952. final List<User> users = new ArrayList<User>();
  953. for (Address address : addresses)
  954. {
  955. String emailAddress = getEmailAddress(address);
  956. if (emailAddress != null)
  957. {
  958. try
  959. {
  960. User user = UserUtils.getUserByEmail(emailAddress);
  961. if (user != null)
  962. {
  963. users.add(user);
  964. }
  965. } catch (EntityNotFoundException entitynotfoundexception)
  966. {
  967. //ignore any emails that dont map to a valid JIRA user
  968. }
  969. }
  970. }
  971. return users;
  972. }
  973. private Collection<Address> getAllNonUserEmails(Address addresses[])
  974. {
  975. if (addresses == null || addresses.length == 0)
  976. {
  977. return Collections.EMPTY_LIST;
  978. }
  979. final List<Address> nonUsers = new ArrayList<Address>();
  980. for (Address address : addresses)
  981. {
  982. String emailAddress = getEmailAddress(address);
  983. if (emailAddress != null)
  984. {
  985. try
  986. {
  987. User user = UserUtils.getUserByEmail(emailAddress);
  988. if (user == null)
  989. {
  990. nonUsers.add(address);
  991. }
  992. } catch (EntityNotFoundException entitynotfoundexception)
  993. {
  994. nonUsers.add(address);
  995. //ignore any emails that dont map to a valid JIRA user
  996. }
  997. }
  998. }
  999. return non

Large files files are truncated, but you can click here to view the full file