/src/main/java/com/redradishtech/jira/ccmailer/CCMailerHandler.java
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
- package com.redradishtech.jira.ccmailer;
- import com.atlassian.core.ofbiz.util.OFBizPropertyUtils;
- import com.atlassian.core.user.UserUtils;
- import com.atlassian.jira.ComponentManager;
- import com.atlassian.jira.config.util.JiraHome;
- import com.atlassian.jira.issue.Issue;
- import com.atlassian.jira.issue.MutableIssue;
- import com.atlassian.jira.issue.comments.Comment;
- import com.atlassian.jira.issue.fields.CustomField;
- import com.atlassian.jira.issue.search.SearchException;
- import com.atlassian.jira.issue.search.SearchProvider;
- import com.atlassian.jira.issue.search.SearchRequest;
- import com.atlassian.jira.issue.watchers.WatcherManager;
- import com.atlassian.jira.jql.parser.DefaultJqlQueryParser;
- import com.atlassian.jira.jql.parser.JqlParseException;
- import com.atlassian.jira.jql.parser.JqlQueryParser;
- import com.atlassian.jira.jql.util.JqlStringSupport;
- import com.atlassian.jira.project.ProjectKeys;
- import com.atlassian.jira.project.ProjectManager;
- import com.atlassian.jira.security.GlobalPermissionManager;
- import com.atlassian.jira.security.Permissions;
- import com.atlassian.jira.security.roles.ProjectRole;
- import com.atlassian.jira.security.roles.ProjectRoleManager;
- import com.atlassian.jira.service.util.ServiceUtils;
- import com.atlassian.jira.service.util.handler.CreateOrCommentHandler;
- import com.atlassian.jira.service.util.handler.RegexCommentHandler;
- import com.atlassian.jira.web.bean.PagerFilter;
- import com.atlassian.mail.MailException;
- import com.atlassian.mail.MailUtils;
- import com.atlassian.mail.server.SMTPMailServer;
- import com.atlassian.query.Query;
- import com.opensymphony.module.propertyset.PropertySet;
- import com.opensymphony.user.EntityNotFoundException;
- import com.opensymphony.user.Group;
- import com.opensymphony.user.User;
- import com.redradishtech.jira.ccmailer.util.CustomFieldHelperBean;
- import com.atlassian.jira.service.util.handler.redradish.CreateIssueHandler;
- import com.atlassian.jira.service.util.handler.redradish.FullCommentHandler;
- import com.atlassian.jira.service.util.handler.redradish.NonQuotedCommentHandler;
- import org.apache.log4j.Logger;
- import org.apache.velocity.exception.VelocityException;
- import org.ofbiz.core.entity.GenericValue;
- import javax.annotation.Nullable;
- import javax.mail.Address;
- import javax.mail.Header;
- import javax.mail.Message;
- import javax.mail.MessagingException;
- import javax.mail.internet.AddressException;
- import javax.mail.internet.InternetAddress;
- import javax.validation.constraints.NotNull;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.text.MessageFormat;
- import java.util.*;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- import java.util.regex.PatternSyntaxException;
- /**
- * A message handler that creates a new issue, or comments on an existing one. Extends the usual {@link CreateOrCommentHandler}
- * behaviour in a few ways:
- * <ul>
- * <li>If 'ccwatcher=true' is set (the default, unlike vanilla CreateOrCommentHandler), Cc'ed users are added to the
- * Watcher list for comments, not just for issue-creating emails.
- * <li>'cccustomfield' must be specified (or the handler refuses to do anything) and must specify the ID or name of a
- * text custom field. This will be populated with email addresses of incoming emails' To: and Cc: fields, providing those emails
- * are not already notified via the Watchers list. This behaviour happens irrespective of the 'ccwatcher' setting.
- * <li>If an email's sender is unknown and the 'reporterusername' parameter sets the Reporter, the handler puts the full
- * email details, including headers, in the issue description.
- * <li>'ccassignee' is false by default (ie. the first CC'ed user does not become the assignee).
- * </ul>
- * There is an optional 'commenttype' field whose default value, 'plaintext', refers to a .vm file on disk. Creating an alternative
- * file and setting this parameter lets you customize the comment format (eg. wiki text vs. plaintext).
- * See the documentation for other fields.
- */
- public class CCMailerHandler extends CreateOrCommentHandler
- {
- private static final Logger log = Logger.getLogger(CCMailerHandler.class);
- private static final Object KEY_PARAMFILE = "paramfile";
- private static final String KEY_COMMENT_TYPE = "commenttype";
- private static final String KEY_CCWATCHER = "ccwatcher";
- private static final String KEY_CC_CUSTOMFIELD = "cccustomfield";
- private static final String KEY_SPLITREGEX = "splitregex";
- private static final String KEY_DONOTWATCHORCC = "donotwatchorcc";
- private static final String KEY_VISIBILITYLEVELS = "visibilitylevels";
- private static final String KEY_SENDERTOPROJECTMAP = "sendertoprojectmap";
- private static final String KEY_BUGZILLA_CUSTOMFIELD = "bugzillafield";
- private static final String KEY_BUGZILLA_QUERY = "bugzillaquery";
- private static final String KEY_BUGZILLA_REGEX = "bugzillaregex";
- /**
- * Whether each Cc'ed user should watch the commented issue. Defaults to true. Analogous to {@link com.redradishtech.jira.ccmailer.util.CreateIssueHandler#CC_WATCHER}
- */
- protected boolean ccWatcher = true;
- private String splitRegex;
- protected String commentType = "plaintext";
- protected String ccCustomField;
- static final String SEMICOLON_SPLIT_REGEX = "(?<!\\\\);"; // split on an unescaped semicolon. The regexp is a zero-width negative lookbehind assertion.
- static final String COLON_SPLIT_REGEX = "(?<!\\\\):"; // split on an unescaped semicolon. The regexp is a zero-width negative lookbehind assertion.
- protected Set<Address> donotWatchOrCc = new HashSet<Address>();
- private SortedMap<String, String> visibilityLevels = new TreeMap<String, String>();
- private SortedMap<String, String> senderToProjectMappings = new TreeMap<String, String>();
- private static final String USER_PROPERTY_KEY = "mailsettings";
- protected String bugzillaField;
- protected String bugzillaQuery = "{0} ~ {1} order by key desc";
- String bugzillaRegex = "Bug (\\d+)";
- public void init(Map params)
- {
- loadParamsFromPropertiesFile(params);
- log.debug("CCMailerHandler.init(params: " + params + ")");
- super.init(params);
- if (params.containsKey(KEY_CCWATCHER))
- {
- ccWatcher = Boolean.valueOf((String) params.get(KEY_CCWATCHER));
- }
- if (params.containsKey(KEY_SPLITREGEX))
- {
- splitRegex = ((String) params.get(KEY_SPLITREGEX));
- }
- if (params.containsKey(KEY_DONOTWATCHORCC))
- {
- String[] bannedEmailStrs = ((String) params.get(KEY_DONOTWATCHORCC)).split(SEMICOLON_SPLIT_REGEX);
- for (String emailStr : bannedEmailStrs)
- {
- try
- {
- donotWatchOrCc.add(new InternetAddress(emailStr));
- } catch (AddressException e)
- {
- log.error("Ignoring invalid " + KEY_DONOTWATCHORCC + " parameter for CCMailerHandler listener: " + e, e);
- }
- }
- }
- if (params.containsKey(KEY_COMMENT_TYPE))
- {
- commentType = (String) params.get(KEY_COMMENT_TYPE);
- }
- if (params.containsKey(KEY_CC_CUSTOMFIELD))
- {
- ccCustomField = (String) params.get(KEY_CC_CUSTOMFIELD);
- }
- if (params.containsKey(KEY_BUGZILLA_CUSTOMFIELD))
- {
- bugzillaField = (String) params.get(KEY_BUGZILLA_CUSTOMFIELD);
- }
- if (params.containsKey(KEY_BUGZILLA_QUERY))
- {
- bugzillaQuery = (String) params.get(KEY_BUGZILLA_QUERY);
- }
- if (params.containsKey(KEY_BUGZILLA_REGEX))
- {
- bugzillaRegex = (String) params.get(KEY_BUGZILLA_REGEX);
- }
- if (params.containsKey(KEY_VISIBILITYLEVELS) && params.get(KEY_VISIBILITYLEVELS) != null)
- {
- String[] vlStrs = ((String) params.get(KEY_VISIBILITYLEVELS)).split(SEMICOLON_SPLIT_REGEX);
- for (String vlStr : vlStrs)
- {
- vlStr = vlStr.replaceAll("\\\\\\;", ";"); // unescape any escaped delimiters
- String[] keyValPair = vlStr.split(COLON_SPLIT_REGEX);
- if (keyValPair.length != 2)
- {
- throw new RuntimeException("CCMailerHandler " + KEY_VISIBILITYLEVELS + " parameter has invalid format. Segment '" + vlStr + "' was expected to contain ':' separating key and value.");
- }
- keyValPair[0] = keyValPair[0].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
- keyValPair[1] = keyValPair[1].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
- visibilityLevels.put(keyValPair[0], keyValPair[1]);
- }
- }
- if (params.containsKey(KEY_SENDERTOPROJECTMAP) && params.get(KEY_SENDERTOPROJECTMAP) != null)
- {
- String[] upStrs = ((String) params.get(KEY_SENDERTOPROJECTMAP)).split(SEMICOLON_SPLIT_REGEX);
- for (String upStr : upStrs)
- {
- upStr = upStr.replaceAll("\\\\\\;", ";"); // unescape any escaped delimiters
- String[] keyValPair = upStr.split(COLON_SPLIT_REGEX);
- if (keyValPair.length != 2)
- {
- throw new RuntimeException("CCMailerHandler " + KEY_SENDERTOPROJECTMAP + " parameter has invalid format. Segment '" + upStr + "' was expected to contain ':' separating key and value.");
- }
- keyValPair[0] = keyValPair[0].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
- keyValPair[1] = keyValPair[1].replaceAll("\\\\\\:", ":"); // unescape any escaped delimiters
- senderToProjectMappings.put(keyValPair[0], keyValPair[1]);
- }
- }
- }
- private void loadParamsFromPropertiesFile(Map params)
- {
- if (params.containsKey(KEY_PARAMFILE))
- {
- String paramFilename = (String) params.get(KEY_PARAMFILE);
- Properties props = new Properties();
- try
- {
- JiraHome jiraHome = ComponentManager.getComponentInstanceOfType(JiraHome.class);
- File propFile = new File(jiraHome.getHome(), paramFilename);
- props.load(new FileInputStream(propFile));
- log.debug("Loading " + this.getClass().getName() + " service properties from " + props);
- Enumeration e = props.keys();
- while (e.hasMoreElements())
- {
- String propName = (String) e.nextElement();
- if (!params.containsKey(propName))
- {
- params.put(propName, props.get(propName));
- }
- }
- } catch (IOException e)
- {
- throw new RuntimeException("Error reading file " + params.get(KEY_PARAMFILE) + ", specified as " + KEY_PARAMFILE + " parameter for a " + getClass().getName() + " email handler. " + e, e);
- }
- }
- }
- // Main logic. The basic structure is the same as CreateOrCommentHandler, overriding called classes to render the full
- // email in the body, setting the Watchers on comments and setting CC for new issues and new comments.
- public boolean handleMessage(Message message) throws MessagingException
- {
- if (!canHandleMessage(message))
- {
- return deleteEmail;
- }
- GenericValue issueGV = getAssociatedIssue(message);
- // We found an issue for this email?
- if (issueGV != null)
- {
- boolean doDelete = false;
- //add the message as a comment to the issueGV
- final Map vl = visibilityLevels;
- if (splitRegex != null)
- {
- log.debug("Creating comment from email with RegexCommentHandler (regex: " + splitRegex + ")");
- RegexCommentHandler rc = new RegexCommentHandler()
- {
- protected String getEmailBody(Message message) throws MessagingException
- {
- User reporter = findReporter(message);
- // Record full email headers if From: address is not known to JIRA
- log.debug("Splitting on regex " + splitRegex + " body:\n" + MailUtils.getBody(message));
- if (reporter != null) return super.getEmailBody(message);
- else return getCommentBody(message, super.getEmailBody(message));
- }
- protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
- {
- final String groupOrRoleName = (String) vl.get(commenterUsername);
- String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
- Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
- return commentManager.create(issue, commenterUsername, body, group, role, false);
- }
- @Override
- protected GenericValue getAssociatedIssue(Message message)
- {
- return myGetAssociatedIssue(message);
- }
- };
- rc.setErrorHandler(this.getErrorHandler());
- rc.init(params);
- doDelete = rc.handleMessage(message);
- }
- else if (stripquotes != null && "true".equalsIgnoreCase(stripquotes)) //if stripquotes not defined in setup
- {
- log.debug("Creating comment from email with NonQuotedCommentHandler");
- NonQuotedCommentHandler nq = new NonQuotedCommentHandler()
- {
- protected String getEmailBody(Message message) throws MessagingException
- {
- User reporter = findReporter(message);
- // Record full email headers if From: address is not known to JIRA
- if (reporter != null) return super.getEmailBody(message);
- else return getCommentBody(message, super.getEmailBody(message));
- }
- protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
- {
- String groupOrRoleName = (String) vl.get(commenterUsername);
- String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
- Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
- return commentManager.create(issue, commenterUsername, body, group, role, false);
- }
- @Override
- protected GenericValue getAssociatedIssue(Message message)
- {
- return myGetAssociatedIssue(message);
- }
- };
- nq.setErrorHandler(this.getErrorHandler());
- nq.init(params);
- doDelete = nq.handleMessage(message); //get message without quotes
- }
- else
- {
- FullCommentHandler fc = new FullCommentHandler()
- {
- protected String getEmailBody(Message message) throws MessagingException
- {
- User reporter = findReporter(message);
- // Record full email headers if From: address is not known to JIRA
- if (reporter != null) return super.getEmailBody(message);
- else return getCommentBody(message, super.getEmailBody(message));
- }
- protected Comment createComment(MutableIssue issue, String commenterUsername, String body)
- {
- String groupOrRoleName = (String) vl.get(commenterUsername);
- String group = getCommentGroup(groupOrRoleName, KEY_VISIBILITYLEVELS);
- Long role = getCommentRole(groupOrRoleName, KEY_VISIBILITYLEVELS);
- return commentManager.create(issue, commenterUsername, body, group, role, false);
- }
- @Override
- protected GenericValue getAssociatedIssue(Message message)
- {
- return myGetAssociatedIssue(message);
- }
- };
- fc.setErrorHandler(this.getErrorHandler());
- fc.init(params);
- doDelete = fc.handleMessage(message); //get message with quotes
- }
- // If the issueGV exists and certain emails were CC'ed, add these emails to the watcher/CC fields *after*
- // adding the comment, so that they do not get two emails (one from the CC, one from JIRA).
- if (ccWatcher)
- {
- myAddCcWatchersToIssue(message, issueGV);
- }
- Issue issueObj = ComponentManager.getInstance().getIssueFactory().getIssue(issueGV);
- // Populate the CC custom field with any email addresses not already a watcher
- populateCCField(message, issueGV, issueObj.getReporter());
- return doDelete;
- }
- else
- { // issueGV == null, so create new issueGV in default project
- CreateIssueHandler createIssueHandler = new CreateIssueHandler()
- {
- public void init(Map params)
- {
- // Default ccassignee to false, but let this be overridden in params if necessary.
- ccAssignee = false;
- super.init(params);
- }
- /**
- * Populate the issueGV summary with full email header details, if it was reported by someone not known
- * to JIRA. Otherwise, create the usual issueGV summary from the message body.
- */
- protected String getDescription(User reporter, Message message) throws MessagingException
- {
- if (reporteruserName != null && reporteruserName.equals(reporter.getName()))
- {
- // From: address doesn't match JIRA user; record full email details in description
- return getCommentBody(message, MailUtils.getBody(message));
- }
- else
- {
- // From: address matched someone; get standard description
- return super.getDescription(reporter, message);
- }
- }
- @Override
- protected GenericValue getProject(Message message)
- {
- User reporter = null;
- List senders = null;
- try
- {
- reporter = findReporter(message);
- senders = MailUtils.getSenders(message);
- } catch (MessagingException e)
- {
- log.error("Error parsing From: address of email " + message + " while processing " + KEY_SENDERTOPROJECTMAP + " param on CCMailerHandler. Default project " + projectKey + " will be used. " + e, e);
- }
- ProjectManager projectManager = ComponentManager.getInstance().getProjectManager();
- String newProjectKey = null;
- // First check if their is a user property explicitly requiring emails from this reporter to be in a certain project
- newProjectKey = getProjectKeyFromUserProperty(reporter);
- if (newProjectKey != null)
- {
- log.debug("Associated user " + reporter + " with project " + newProjectKey + " via user property");
- }
- else if (senders != null && senderToProjectMappings != null)
- {
- // Keys here will be strings like {group}jira-developers or {email}.*@localhost
- for (String groupOrEmailStr : senderToProjectMappings.keySet())
- {
- String mappedProjectKey = senderToProjectMappings.get(groupOrEmailStr);
- if (projectManager.getProjectObjByKey(mappedProjectKey) == null)
- {
- log.warn("Mapping " + groupOrEmailStr + ":" + mappedProjectKey + " in " + KEY_SENDERTOPROJECTMAP + " param of CCMailerListneer is invalid; project '" +
- mappedProjectKey + "' does not exist. Using default project " + projectKey);
- continue;
- }
- String group = getCommentGroup(groupOrEmailStr, KEY_SENDERTOPROJECTMAP);
- if (reporter != null && reporter.inGroup(group))
- {
- log.debug("Email sender " + reporter + " in group " + group);
- if (newProjectKey != null)
- {
- log.warn("Two items in " + KEY_SENDERTOPROJECTMAP + " matched user " + reporter + ". Giving up and using default project " + projectKey);
- newProjectKey = null;
- break;
- }
- else
- newProjectKey = mappedProjectKey;
- }
- Pattern p = getCommentEmailRegex(groupOrEmailStr, KEY_SENDERTOPROJECTMAP);
- if (p != null)
- {
- for (Object senderEmail : senders)
- {
- Matcher m = p.matcher("" + senderEmail);
- if (m.find())
- {
- log.debug("Email sender's email " + senderEmail + " matched regex " + p);
- if (newProjectKey != null)
- {
- log.warn("Two items in " + KEY_SENDERTOPROJECTMAP + " matched user " + reporter + ". Giving up and using default project " + projectKey);
- newProjectKey = null;
- break;
- }
- else
- newProjectKey = mappedProjectKey;
- }
- else
- {
- log.debug("Email '" + senderEmail + "' does not match specified regex " + p);
- }
- }
- }
- }
- if (newProjectKey != null)
- {
- log.debug("Email sender " + reporter + " had " + KEY_SENDERTOPROJECTMAP + " mapping; using mapped project key " + newProjectKey);
- }
- }
- if (newProjectKey != null)
- {
- projectKey = newProjectKey;
- return projectManager.getProjectByKey(newProjectKey);
- }
- else
- {
- return super.getProject(message);
- }
- }
- /**
- * Override the point at which users are added to Watchers to also add emails to the CC field.
- * Note that by this point the issue has already been created, and all events fired, so neither new watchers
- * not CC emails get notified of the event.
- */
- public void addCcWatchersToIssue(Message message, GenericValue issueGV, User reporter) throws MessagingException
- {
- // Any recognised emails become watchers
- myAddCcWatchersToIssue(message, issueGV);
- populateCCField(message, issueGV, reporter);
- }
- };
- createIssueHandler.setErrorHandler(this.getErrorHandler());
- createIssueHandler.init(params);
- return createIssueHandler.handleMessage(message);
- }
- }
- @Override
- protected GenericValue getAssociatedIssue(Message message)
- {
- return myGetAssociatedIssue(message);
- }
- /**
- * Determines which issue is referred to by the given email.
- *
- * @param message Email
- * @return Associated issue, or null of none associated.
- */
- public GenericValue myGetAssociatedIssue(@NotNull Message message)
- {
- String subject = null;
- try
- {
- subject = message.getSubject();
- } catch (MessagingException e)
- {
- log.error(e, e);
- return null;
- }
- GenericValue issueGV = ServiceUtils.findIssueInString(subject); // Explicit key in subject?
- if (issueGV == null) issueGV = findIssueFromBugzillaIdInString(subject); // Otherwise is there a Bugzilla ID?
- if (issueGV == null) issueGV = super.getAssociatedIssue(message); // Otherwise does In-Reply-To: match?
- return issueGV;
- }
- /**
- * Returns a project associated with the user via a JIRA user property.
- *
- * @param user The user to look up (possibly null)
- * @return A valid JIRA project key if one is associated with this user, otherwise null.
- */
- private
- @Nullable
- String getProjectKeyFromUserProperty(@Nullable User user)
- {
- if (user == null) return null;
- PropertySet propertySet = user.getPropertySet();
- String actualPropertyKeyName = null;
- for (Object key : propertySet.getKeys("jira.meta."))
- {
- if (key instanceof String && ("jira.meta." + USER_PROPERTY_KEY).equalsIgnoreCase((String) key))
- {
- actualPropertyKeyName = (String) key;
- break;
- }
- }
- if (actualPropertyKeyName != null && propertySet.exists(actualPropertyKeyName))
- { // exists() should be redundant
- String keyValue = propertySet.getString(actualPropertyKeyName);
- String[] keyValPair = keyValue.split("(?<!\\\\)="); // unescaped equal sign
- if (keyValPair.length != 2)
- {
- log.error("CCMailerHandler user " + user + "'s property " + actualPropertyKeyName + "' has an invalid format. Segment '" + keyValue + "' was expected to contain '=' separating key and value.");
- return null;
- }
- if (!"project".equals(keyValPair[0]))
- {
- log.error("Unexpected property key " + keyValPair[0] + " in user " + user + " property " + actualPropertyKeyName);
- return null;
- }
- ProjectManager projectManager = ComponentManager.getInstance().getProjectManager();
- String projectKey = keyValPair[1];
- if (projectManager.getProjectObjByKey(projectKey) == null)
- {
- 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 +
- "' does not specify a valid project. Ignoring the user property.");
- }
- else return projectKey;
- }
- return null;
- }
- private Pattern getCommentEmailRegex(String groupOrEmailName, String paramName)
- {
- if (groupOrEmailName != null)
- if (groupOrEmailName.startsWith("{email}"))
- {
- String emailRegexStr = groupOrEmailName.substring("{email}".length());
- try
- {
- return Pattern.compile(emailRegexStr);
- } catch (PatternSyntaxException e)
- {
- log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies invalid regex " + emailRegexStr + "\n" + e, e);
- return null;
- }
- }
- return null;
- }
- /**
- * Given a group/role specifier from visibilitylevels param, return the role ID if a role is referred to, or null otherwise.
- *
- * @param groupOrRoleName Eg. '{role}Users' or 'jira-developers' (a group hence returns null), or null
- * @param paramName Parameter the groupOrRoleName came from, for logging/debugging purposes.
- * @return Group id, eg. 10000 for Users
- */
- @Nullable
- Long getCommentRole(@Nullable String groupOrRoleName, String paramName)
- {
- if (groupOrRoleName != null)
- if (groupOrRoleName.startsWith("{role}"))
- {
- String roleName = groupOrRoleName.substring("{role}".length());
- ProjectRole role = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class).getProjectRole(roleName);
- if (role == null)
- {
- log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies nonexistent role " + roleName);
- }
- else
- {
- return role.getId();
- }
- }
- return null;
- }
- /**
- * Given a group/role specifier from visibilitylevels param, returns the group name if a group is referred to, or null otherwise.
- *
- * @param groupOrRoleName Eg. 'jira-developers', '{group}jira-developers' (equivalent), or a role name or null.
- * @param paramName Name of the handler parameter that contained groupOrRoleName; used only for logging.
- * @return Group string, eg. 'jira-developers' if it is valid, null otherwise.
- */
- @Nullable
- String getCommentGroup(@Nullable String groupOrRoleName, final String paramName)
- {
- if (groupOrRoleName != null)
- if (groupOrRoleName.startsWith("{group}") || !groupOrRoleName.startsWith("{"))
- {
- String groupName = groupOrRoleName.startsWith("{group}") ? groupOrRoleName.substring("{group}".length()) : groupOrRoleName;
- Group group = ComponentManager.getInstance().getUserUtil().getGroup(groupName);
- if (group == null)
- {
- log.error("CCMailerHandler " + paramName + " param (probably in WEB-INF/classes/ccmailer.properties) specifies nonexistent group " + groupName);
- }
- else
- {
- return groupName;
- }
- }
- return null;
- }
- /**
- * If the given email subject contains a Bugzilla reference (eg. 'DO NOT REPLY (Bug 12345) Blah blah..') then return the JIRA issue
- * having a Bugzilla custom field with the referenced value (eg. 12345)
- *
- * @param subject Email subject
- * @return Associated issue, or null if none found or lookup failed.`
- */
- private GenericValue findIssueFromBugzillaIdInString(String subject)
- {
- if (bugzillaField == null || bugzillaQuery == null || bugzillaRegex == null) return null;
- if (subject == null)
- {
- log.warn("Email has null subject; not associating with any issue.");
- return null;
- }
- try
- {
- Pattern p;
- try
- {
- p = Pattern.compile(bugzillaRegex);
- } catch (PatternSyntaxException e)
- {
- log.error("Invalid " + KEY_BUGZILLA_REGEX + " parameter in " + this.getClass().getName() + " service instance. Parameter value '" + bugzillaRegex + "' is not a valid regular expression: " + e, e);
- return null;
- }
- Matcher m = p.matcher(subject);
- if (m.find())
- {
- String bugIdStr = m.group(1);
- log.debug("Found reference to Bugzilla ticket " + bugIdStr + ". Searching custom field '" + bugzillaField + "' for it..");
- JqlStringSupport supp = (JqlStringSupport) ComponentManager.getInstance().getContainer().getComponentInstanceOfType(JqlStringSupport.class);
- JqlQueryParser jqlParser = new DefaultJqlQueryParser();
- String jqlQueryStr = null;
- try
- {
- jqlQueryStr = MessageFormat.format(bugzillaQuery, supp.encodeFieldName(bugzillaField), supp.encodeStringValue(bugIdStr));
- } catch (IllegalArgumentException e)
- {
- 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);
- return null;
- }
- log.debug("Searching for issue with Bugzilla ID: running JQL query: " + jqlQueryStr);
- Query jqlQuery = null;
- try
- {
- jqlQuery = jqlParser.parseQuery(jqlQueryStr);
- } catch (JqlParseException e)
- {
- log.error("Unexpected error parsing JQL '" + jqlQueryStr + "', constructed from " + KEY_BUGZILLA_QUERY + " parameter '" + bugzillaQuery + "': " + e, e);
- return null;
- }
- SearchRequest sr = new SearchRequest(jqlQuery);
- // SearchRequest sr = new SearchRequest(cb.buildQuery());
- // return validateAndSearch(user, searchRequest, pagerFilter);
- SearchProvider searcher = ComponentManager.getInstance().getSearchProvider();
- try
- {
- List<Issue> issues = searcher.searchOverrideSecurity(sr.getQuery(), getAnyAdminUser(), PagerFilter.getUnlimitedFilter(), null).getIssues();
- if (issues == null || issues.size() == 0)
- {
- log.debug("No issues found with " + bugzillaField + " cf value " + bugIdStr);
- }
- Issue i = issues.get(0);
- if (issues.size() > 1)
- {
- StringBuffer buf = new StringBuffer();
- for (Issue ii : issues) buf.append(ii.getKey()).append(" ");
- log.warn("Found more than 1 (" + issues.size() + ") issues matching " + jqlQueryStr + " ( " + buf + "). Associating comment with first (" + i.getKey() + ")");
- }
- log.debug("Issue " + i.getKey() + " matches " + jqlQueryStr);
- return ServiceUtils.getIssue(i.getKey());
- } catch (SearchException e)
- {
- log.error(e, e);
- } catch (EntityNotFoundException e)
- {
- log.error(e, e);
- }
- }
- } catch (Exception e)
- {
- log.error("Error extracting bugzilla ID from subject: " + e, e);
- return null;
- }
- return null;
- }
- // Thanks Jamie Echlin http://forums.atlassian.com/thread.jspa?messageID=257334242�
- public User getAnyAdminUser() throws EntityNotFoundException
- {
- GlobalPermissionManager globalPermissionManager = (GlobalPermissionManager) ComponentManager.getInstance().getComponentInstanceOfType(GlobalPermissionManager.class);
- Collection groups = globalPermissionManager.getGroups(Permissions.ADMINISTER);
- for (Iterator iterator = groups.iterator(); iterator.hasNext(); )
- {
- Group group = (Group) iterator.next();
- for (Iterator userIterator = group.getUsers().iterator(); userIterator.hasNext(); )
- {
- String userName = (String) userIterator.next();
- return UserUtils.getUser(userName);
- }
- }
- return null;
- }
- /**
- * Populate the CC custom field with any email addresses not already a watcher, reporter or default JIRA address.
- */
- private void populateCCField(Message message, GenericValue issueGV, User reporter)
- {
- try
- {
- if (ccCustomField != null)
- {
- MutableIssue issue = ComponentManager.getInstance().getIssueFactory().getIssue(issueGV);
- Set<Address> unnotifiedEmails = findEmailsToCC(message, issue, reporter);
- CustomFieldHelperBean cfHelper = new CustomFieldHelperBean(ComponentManager.getInstance().getCustomFieldManager(), log);
- CustomField cf = cfHelper.lookupCustomField(ccCustomField);
- if (cf != null)
- {
- Set<InternetAddress> alreadyCCedEmails = cfHelper.getCustomFieldValueSet(issue, cf);
- unnotifiedEmails.removeAll(alreadyCCedEmails);
- try
- {
- cfHelper.addCustomFieldValueSet(issue, cf, unnotifiedEmails);
- for (Address email : unnotifiedEmails)
- {
- log.info("Added to " + issue.getKey() + " " + ccCustomField + " list: " + email);
- }
- } catch (Exception e)
- {
- log.error("Error setting custom field value " + unnotifiedEmails + " for cf " + cf + " to issue " + issue + ". Not setting CC field. Error is: " + e, e);
- }
- }
- }
- else
- {
- log.warn("No cccustomfield parameter specified for a CCMailerHandler");
- }
- } catch (Throwable t)
- {
- log.error("Error populating CC field: " + t, t);
- }
- }
- /**
- * Don't handle the message unless the cccustomfield parameter has been specified.
- *
- * @param message message to check if it can be handled
- * @return If we should attempt to handle this email.
- */
- protected boolean canHandleMessage(Message message)
- {
- if (ccCustomField == null)
- {
- log.error("CCMailerHandler specified, but not configured with a cccustomfield parameter. Not handling the email.");
- return false;
- }
- return super.canHandleMessage(message);
- }
- /**
- * Searches an email's To, Cc & similar fields, returning those that aren't yet (but could be) notified of issue updates,
- * and who aren't existing users in JIRA. This means excluding watchers, the default JIRA From: address (eg. jira@example.com) and catchEmail address.
- * 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.
- */
- private Set<Address> findEmailsToCC(Message message, MutableIssue issue, User reporter) throws MessagingException
- {
- // Start by assuming all To: Cc: etc. fields want to receive future updates, and should be added to the CC field
- Set<Address> unnotifiedEmails = new HashSet<Address>();
- for (Address a : message.getAllRecipients())
- {
- unnotifiedEmails.add(a);
- }
- // The email sender presumably is interested and wants future updates. If this email address matches 'reporter'
- // username address it will be removed later
- unnotifiedEmails.addAll(getAllNonUserEmails(message.getFrom()));
- // Don't CC people who are already Watchers
- final WatcherManager watchermanager = ComponentManager.getInstance().getWatcherManager();
- Collection<User> currentNotifiedUserList = watchermanager.getCurrentWatchList(Locale.getDefault(), issue.getGenericValue());
- for (User user : currentNotifiedUserList)
- {
- unnotifiedEmails.remove(new InternetAddress(user.getEmail()));
- }
- // Don't CC the official issue reporter - if they want notifications they'll add this to the notification scheme
- if (reporter != null) unnotifiedEmails.remove(new InternetAddress(reporter.getEmail()));
- // Remove the JIRA address, eg. jira@example.com, otherwise we'll be creating a mail loop.
- Address fromEmailAddress = getJIRAFromAddress(issue);
- if (fromEmailAddress != null && unnotifiedEmails.contains(fromEmailAddress))
- {
- log.debug("Not adding " + fromEmailAddress + " to CC list as it is this project's default From address");
- unnotifiedEmails.remove(fromEmailAddress);
- }
- // Remove the JIRA address, indicated as catchemail=... in the handler params.
- if (catchEmail != null)
- {
- try
- {
- InternetAddress catchEmailAddr = new InternetAddress(catchEmail);
- if (unnotifiedEmails.contains(catchEmailAddr))
- {
- log.debug("Not adding " + catchEmail + " to CC list as it is this handler's catchEmail value");
- unnotifiedEmails.remove(catchEmailAddr);
- }
- } catch (AddressException e)
- {
- log.error("Invalid 'catchemail' address; not stripping it from CC list for " + issue);
- }
- }
- // Remove banned email addresses
- unnotifiedEmails.removeAll(donotWatchOrCc);
- return unnotifiedEmails;
- }
- protected void recordMessageId(String type, Message message, Long issueId) throws MessagingException
- {
- super.recordMessageId(type, message, issueId);
- }
- /**
- * Return the email address that JIRA mails generated for the specified issue will come from.
- * For example, JIRA emails might come from jira@example.com.
- *
- * @param issue Issue
- * @return Email address, or null if a valid email address is not set.
- */
- private InternetAddress getJIRAFromAddress(MutableIssue issue)
- {
- ComponentManager componentManager = ComponentManager.getInstance();
- ProjectManager projectManager = componentManager.getProjectManager();
- GenericValue project = projectManager.getProject(issue.getGenericValue());
- if (project == null)
- {
- throw new IllegalArgumentException("Project not found for issue " + issue);
- }
- PropertySet projectPS = OFBizPropertyUtils.getPropertySet(project);
- String fromAddressStr = projectPS.getString(ProjectKeys.EMAIL_SENDER);
- // If there is no project-specific From address, look for the default.
- if (fromAddressStr == null)
- {
- final SMTPMailServer defaultSMTPMailServer;
- try
- {
- defaultSMTPMailServer = componentManager.getMailServerManager().getDefaultSMTPMailServer();
- } catch (MailException e)
- {
- log.error("Error looking up default SMTP server. Ignoring", e);
- return null;
- }
- if (defaultSMTPMailServer != null)
- fromAddressStr = defaultSMTPMailServer.getDefaultFrom();
- }
- InternetAddress fromAddress = null;
- try
- {
- fromAddress = new InternetAddress(fromAddressStr);
- } catch (AddressException e)
- {
- log.error("The email address '" + fromAddressStr + " used as 'From' address for " + issue + " issues is invalid", e);
- }
- return fromAddress;
- }
- /**
- * Renders an email's text in a JIRA comment. By default this means showing all the email headers, then the email body.
- *
- * @param message email
- * @param emailBody Email body text in printable form (from superclass usually).
- * @return Comment body string.
- * @throws MessagingException If there was some error extracting the email contents.
- */
- protected String getCommentBody(Message message, String emailBody) throws MessagingException
- {
- String result = null;
- Map<String, Object> params = new HashMap<String, Object>();
- Enumeration allHeaders = message.getAllHeaders();
- HashMap<String, String> headers = new HashMap<String, String>();
- while (allHeaders.hasMoreElements())
- {
- Header header = (Header) allHeaders.nextElement();
- headers.put(header.getName(), header.getValue());
- }
- params.put("subject", message.getSubject());
- params.put("headers", headers);
- params.put("cc", message.getHeader("cc"));
- params.put("from", message.getFrom());
- params.put("body", emailBody);
- try
- {
- result = ComponentManager.getInstance().getVelocityManager().getBody("", "templates/ccmailer/comment/" + commentType + ".vm", params);
- } catch (VelocityException e)
- {
- log.error("Error rendering Cc notification", e);
- }
- return result;
- }
- /**
- * Adds all valid users that are in the email To and cc fields as watchers of the issue.
- * 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)}.
- * Reimplemented here to allow the behaviour for new comments, not just new issues.
- *
- * @param message message to extract the email addresses from
- * @param issue issue to add the watchers to
- * @throws MessagingException message errors
- */
- public void myAddCcWatchersToIssue(Message message, GenericValue issue)
- {
- try
- {
- Collection<User> users = getAllUsersFromEmails(message.getAllRecipients());
- User emailSender = getReporter(message);
- try
- {
- if (!emailSender.equals(UserUtils.getUser(reporteruserName)))
- {
- users.add(emailSender);
- }
- } catch (EntityNotFoundException e)
- {
- log.warn("Nonexistent reporterusername user: " + reporteruserName);
- }
- users = removeUserMatchingEmails(users, donotWatchOrCc, "Not watching %s as it is in the " + KEY_DONOTWATCHORCC + " parameter.");
- if (!users.isEmpty())
- {
- final WatcherManager watchermanager = ComponentManager.getInstance().getWatcherManager();
- for (User user : users)
- {
- watchermanager.startWatching(user, issue);
- log.debug("Email address " + user.getEmail() + " added as watcher");
- }
- }
- } catch (Throwable t)
- {
- log.error("Error adding CC'ed JIRA users to watchers: " + t, t);
- }
- }
- private Collection<User> removeUserMatchingEmails(final Collection<User> users, Set<Address> donotWatchOrCc, String debugMsg) throws AddressException
- {
- List<User> newUsers = new ArrayList<User>();
- for (User u : users)
- {
- InternetAddress addr = new InternetAddress(u.getEmail());
- if (!donotWatchOrCc.contains(addr))
- {
- newUsers.add(u);
- }
- else
- log.debug(String.format(debugMsg, u));
- }
- return newUsers;
- }
- private Collection<User> getAllUsersFromEmails(Address addresses[])
- {
- return getAllUsersFromEmails(Arrays.asList(addresses));
- }
- private List<User> getAllUsersFromEmails(Collection<Address> addresses)
- {
- if (addresses == null || addresses.size() == 0)
- {
- return Collections.EMPTY_LIST;
- }
- final List<User> users = new ArrayList<User>();
- for (Address address : addresses)
- {
- String emailAddress = getEmailAddress(address);
- if (emailAddress != null)
- {
- try
- {
- User user = UserUtils.getUserByEmail(emailAddress);
- if (user != null)
- {
- users.add(user);
- }
- } catch (EntityNotFoundException entitynotfoundexception)
- {
- //ignore any emails that dont map to a valid JIRA user
- }
- }
- }
- return users;
- }
- private Collection<Address> getAllNonUserEmails(Address addresses[])
- {
- if (addresses == null || addresses.length == 0)
- {
- return Collections.EMPTY_LIST;
- }
- final List<Address> nonUsers = new ArrayList<Address>();
- for (Address address : addresses)
- {
- String emailAddress = getEmailAddress(address);
- if (emailAddress != null)
- {
- try
- {
- User user = UserUtils.getUserByEmail(emailAddress);
- if (user == null)
- {
- nonUsers.add(address);
- }
- } catch (EntityNotFoundException entitynotfoundexception)
- {
- nonUsers.add(address);
- //ignore any emails that dont map to a valid JIRA user
- }
- }
- }
- return non…
Large files files are truncated, but you can click here to view the full file