/* * Copyright (c) 2002-2004 * All rights reserved. */ package com.atlassian.jira.service.util.handler; import com.atlassian.core.user.UserUtils; import com.atlassian.core.util.collection.EasyList; import com.atlassian.jira.ComponentManager; import com.atlassian.jira.ManagerFactory; import com.atlassian.jira.config.ConstantsManager; import com.atlassian.jira.issue.IssueFieldConstants; import com.atlassian.jira.issue.IssueImpl; import com.atlassian.jira.issue.MutableIssue; import com.atlassian.jira.issue.fields.CustomField; import com.atlassian.jira.issue.fields.SummarySystemField; import com.atlassian.jira.issue.issuetype.IssueType; import com.atlassian.jira.issue.security.IssueSecurityLevelManager; import com.atlassian.jira.issue.watchers.WatcherManager; import com.atlassian.jira.mail.MailThreadManager; import com.atlassian.jira.project.Project; import com.atlassian.jira.project.ProjectManager; import com.atlassian.jira.security.PermissionManager; import com.atlassian.jira.security.Permissions; import com.atlassian.jira.util.ErrorCollection; import com.atlassian.jira.util.I18nHelper; import com.atlassian.jira.util.SimpleErrorCollection; import com.atlassian.jira.web.action.issue.IssueCreationHelperBean; import com.atlassian.jira.web.bean.FieldVisibilityBean; import com.atlassian.jira.web.bean.I18nBean; import com.atlassian.jira.web.FieldVisibilityManager; import com.atlassian.jira.workflow.WorkflowFunctionUtils; import com.atlassian.mail.MailUtils; import com.opensymphony.user.EntityNotFoundException; import com.opensymphony.user.User; import com.opensymphony.util.TextUtils; import org.apache.log4j.Logger; import org.ofbiz.core.entity.GenericValue; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import javax.mail.Address; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Part; import javax.mail.internet.InternetAddress; /** * A message handler to create a new issue from an incoming message. Note: requires public noarg constructor as this * class is instantiated by reflection */ public class CreateIssueHandler extends AbstractMessageHandler { private static final Logger log = Logger.getLogger(CreateIssueHandler.class); private static final String KEY_PROJECT = "project"; private static final String KEY_ISSUETYPE = "issuetype"; private static final String CC_ASSIGNEE = "ccassignee"; private static final String CC_WATCHER = "ccwatcher"; public String projectKey; // default project where new issues are created public String issueType; // default type for new issues public boolean ccAssignee = true; // Whether the first existing Cc'ed user becomes the assignee public boolean ccWatcher = false; // Whether each Cc'ed user should watch the created issue public void init(Map params) { log.debug("CreateIssueHandler.init(params: " + params + ")"); super.init(params); if (params.containsKey(KEY_PROJECT)) { projectKey = (String) params.get(KEY_PROJECT); } if (params.containsKey(KEY_ISSUETYPE)) { issueType = (String) params.get(KEY_ISSUETYPE); } if (params.containsKey(CC_ASSIGNEE)) { ccAssignee = Boolean.valueOf((String) params.get(CC_ASSIGNEE)); } if (params.containsKey(CC_WATCHER)) { ccWatcher = Boolean.valueOf((String) params.get(CC_WATCHER)); } } public boolean handleMessage(Message message) throws MessagingException { log.debug("CreateIssueHandler.handleMessage"); if (!canHandleMessage(message)) { return deleteEmail; } try { // get either the sender of the message, or the default reporter User reporter = getReporter(message); // no reporter - so reject the message if (reporter == null) { log.warn("Sender is anonymous, no default reporter specified and creating users is set to false (or external user managment is enabled). Message rejected."); addError(getI18nBean().getText("admin.mail.no.default.reporter")); return false; } GenericValue project = getProject(message); log.debug("Project = " + project); if (project == null) { log.warn("Cannot handle message as destination project is null"); addError(getI18nBean().getText("admin.mail.no.project.configured")); return false; } ProjectManager projectManager = ComponentManager.getInstance().getProjectManager(); Project projectObj = projectManager.getProjectObj(project.getLong("id")); // Check that the license is valid before allowing issues to be created // This checks for: evaluation licenses expired, user limit licenses where limit has been exceeded IssueCreationHelperBean issueCreationHelperBean = ComponentManager.getInstance().getIssueCreationHelperBean(); ErrorCollection errorCollection = new SimpleErrorCollection(); // Note: want English locale here for logging purposes I18nHelper i18nHelper = new I18nBean(Locale.ENGLISH); issueCreationHelperBean.validateLicense(errorCollection, i18nHelper); if (errorCollection.hasAnyErrors()) { log.warn("Cannot create issue due to invalid license: " + errorCollection.getErrorMessages().toString()); addError(getI18nBean().getText("admin.mail.bad.license", errorCollection.getErrorMessages().toString())); return false; } // If user does not have create permissions, there's no point proceeding. Error out here to avoid a stack // trace blow up from the WorkflowManager later on. PermissionManager permissionManager = ComponentManager.getInstance().getPermissionManager(); if (!permissionManager.hasPermission(Permissions.CREATE_ISSUE, projectObj, reporter, true)) { log.warn("Reporter (" + reporter.getName() + ") does not have permission to create an issue. Message rejected."); addError(getI18nBean().getText("admin.mail.no.create.permission", reporter.getName())); return false; } log.debug("Issue Type Key = = " + issueType); if (!hasValidIssueType()) { log.warn("Cannot handle message as Issue Type is null or invalid"); addError(getI18nBean().getText("admin.mail.invalid.issue.type")); return false; } String summary = message.getSubject(); if (!TextUtils.stringSet(summary)) { addError(getI18nBean().getText("admin.mail.no.subject")); return false; } if (summary.length() > SummarySystemField.MAX_LEN.intValue()) { log.warn("Truncating summary field because it is too long: " + summary); summary = summary.substring(0, SummarySystemField.MAX_LEN.intValue() - 3) + "..."; } // JRA-7646 - check if priority/description is hidden - if so, do not set String priority = null; String description = null; FieldVisibilityManager visibility = new FieldVisibilityBean(); if (!visibility.isFieldHiddenInAllSchemes(project.getLong("id"), IssueFieldConstants.PRIORITY, EasyList.build(issueType))) { priority = getPriority(message); } if (!visibility.isFieldHiddenInAllSchemes(project.getLong("id"), IssueFieldConstants.DESCRIPTION, EasyList.build(issueType))) { description = getDescription(reporter, message); } MutableIssue issueObject = IssueImpl.getIssueObject(null); issueObject.setProject(project); issueObject.setSummary(summary); issueObject.setDescription(description); issueObject.setIssueTypeId(issueType); issueObject.setReporter(reporter); // if no valid assignee found, attempt to assign to default assignee User assignee = null; if (ccAssignee) { assignee = getFirstValidAssignee(message.getAllRecipients(), project); } if (assignee == null) { assignee = ComponentManager.getInstance().getAssigneeResolver().getDefaultAssignee(issueObject, Collections.EMPTY_MAP); } if (assignee != null) { issueObject.setAssignee(assignee); } issueObject.setPriorityId(priority); // Ensure issue level security is correct setDefaultSecurityLevel(issueObject); /* * + FIXME -- set cf defaults @todo + */ // set default custom field values // CustomFieldValuesHolder cfvh = new CustomFieldValuesHolder(issueType, project.getLong("id")); // fields.put("customFields", CustomFieldUtils.getCustomFieldValues(cfvh.getCustomFields())); Map fields = new HashMap(); fields.put("issue", issueObject); // TODO: How is this supposed to work? There is no issue created yet; ID = null. GenericValue origianlIssueGV = ComponentManager.getInstance().getIssueManager().getIssue(issueObject.getId()); // Give the CustomFields a chance to set their default values JRA-11762 List customFieldObjects = ComponentManager.getInstance().getCustomFieldManager().getCustomFieldObjects(issueObject); for (CustomField customField : customFieldObjects) { issueObject.setCustomFieldValue(customField, customField.getDefaultValue(issueObject)); } fields.put(WorkflowFunctionUtils.ORIGINAL_ISSUE_KEY, issueFactory.getIssue(origianlIssueGV)); GenericValue issue = ManagerFactory.getIssueManager().createIssue(reporter, fields); if (issue != null) { log.info("Issue " + issue.get("key") + " created"); // Add Cc'ed users as watchers if params set - JRA-9983 if (ccWatcher) { addCcWatchersToIssue(message, issue, reporter); } // Record the message id of this e-mail message so we can track replies to this message // and associate them with this issue recordMessageId(MailThreadManager.ISSUE_CREATED_FROM_EMAIL, message, issue.getLong("id")); } // TODO: if this throws an error, then the issue is already created, but the email not deleted - we will keep "handling" this email over and over :( createAttachmentsForMessage(message, issue); return true; } catch (Exception e) { log.warn("Could not create issue from message!", e); addError(getI18nBean().getText("admin.mail.unable.to.create.issue"), e); } // something went wrong - don't delete the message return false; } /** * Adds all valid users that are in the email to and cc fields as watchers of the issue. * * @param message message to extract the email addresses from * @param issue issue to add the watchers to * @param reporter * @throws MessagingException message errors */ public void addCcWatchersToIssue(Message message, GenericValue issue, User reporter) throws MessagingException { Collection users = getAllUsersFromEmails(message.getAllRecipients()); //we don't want to add the reporter to the watchers list, lets get rid of him. users.remove(reporter); if (!users.isEmpty()) { final WatcherManager watchermanager = ComponentManager.getInstance().getWatcherManager(); for (User user : users) { watchermanager.startWatching(user, issue); } } } public Collection getAllUsersFromEmails(Address addresses[]) { if (addresses == null || addresses.length == 0) { return Collections.emptyList(); } final List users = new ArrayList(); 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 String getEmailAddress(Address address) { if (address instanceof InternetAddress) { InternetAddress internetaddress = (InternetAddress) address; return internetaddress.getAddress(); } return null; } protected GenericValue getProject(Message message) { // if there is no project then the issue cannot be created if (projectKey == null) { log.debug("Project key NOT set. Cannot find project."); return null; } log.debug("Project key = " + projectKey); return getProjectManager().getProjectByKey(projectKey.toUpperCase(Locale.getDefault())); } protected boolean hasValidIssueType() { // if there is no project then the issue cannot be created if (issueType == null) { log.debug("Issue Type NOT set. Cannot find Issue type."); return false; } IssueType issueTypeObject = ManagerFactory.getConstantsManager().getIssueTypeObject(issueType); if (issueTypeObject == null) { log.debug("Issue Type with does not exist with id of " + issueType); return false; } log.debug("Issue Type Object = " + issueTypeObject.getName()); return true; } protected ProjectManager getProjectManager() { return ManagerFactory.getProjectManager(); } /** * Extracts the description of the issue from the message * * @param reporter the established reporter of the issue * @param message the message from which the issue is created * @return the description of the issue * @throws MessagingException if cannot find out who is the message from */ private String getDescription(User reporter, Message message) throws MessagingException { return recordFromAddressForAnon(reporter, message, MailUtils.getBody(message)); } /** * Adds the senders' from addresses to the end of the issue's details (if they could be extracted), if the e-mail * has been received from an unknown e-mail address and the mapping to an "anonymous" user has been enabled. * * @param reporter the established reporter of the issue (after one has been established) * @param message the message that is used to create issue * @param description the issues exracted description * @return the modified description if the e-mail is from anonymous user, unmodified description otherwise * @throws MessagingException if cannot find out who is the message from */ private String recordFromAddressForAnon(User reporter, Message message, String description) throws MessagingException { // If the message has been created for an anonymous user add the senders e-mail address to the description. if (reporteruserName != null && reporteruserName.equals(reporter.getName())) { description += "\n[Created via e-mail "; if (message.getFrom() != null && message.getFrom().length > 0) { description += "received from: " + message.getFrom()[0] + "]"; } else { description += "but could not establish sender's address.]"; } } return description; } /** * Using the first 'X-Priority' from the message, get the issue's priority * * @param message message * @return message priority * @throws MessagingException if cannot read message's header */ private String getPriority(Message message) throws MessagingException { ConstantsManager constantsManager = ManagerFactory.getConstantsManager(); String[] xPrioHeaders = message.getHeader("X-Priority"); if (xPrioHeaders != null && xPrioHeaders.length > 0) { String xPrioHeader = xPrioHeaders[0]; int priorityValue = Integer.parseInt(TextUtils.extractNumber(xPrioHeader)); if (priorityValue == 0) { return getDefaultSystemPriority(); } // if priority is unset - pick the closest priority, this should be a sensible default Collection priorities = constantsManager.getPriorities(); Iterator priorityIt = priorities.iterator(); /* * NOTE: Valid values for X-priority are (1=Highest, 2=High, 3=Normal, 4=Low & 5=Lowest) The X-Priority * (priority in email header) is divided by 5 (number of valid values) this gives the percentage * representation of the priority. We multiply this by the priority.size() (number of priorities in jira) to * scale and map the percentage to a priority in jira. */ int priorityNumber = (int) Math.ceil(((double) priorityValue / 5d) * (double) priorities.size()); // if priority is too large, assume its the 'lowest' if (priorityNumber > priorities.size()) { priorityNumber = priorities.size(); } String priority = null; for (int i = 0; i < priorityNumber; i++) { priority = ((GenericValue) priorityIt.next()).getString("id"); } return priority; } else { return getDefaultSystemPriority(); } } /** * Returns a default system priority. If default system priority if not * set, tries to find 'middle' priority based on other priorities. It may * throw RuntimeException if there is not default priority set and there * are no other priorities (which is highly unlikely). * * @return a default system priority * @throws RuntimeException if no default set and no other priorities found. */ private String getDefaultSystemPriority() { // if priority header is not set, assume it's 'default' ConstantsManager constantsManager = ManagerFactory.getConstantsManager(); GenericValue defaultPriority = constantsManager.getDefaultPriority(); if (defaultPriority == null) { log.error("Default priority was null. Using the 'middle' priority."); Collection priorities = constantsManager.getPriorities(); final int times = (int) Math.ceil((double) priorities.size() / 2d); Iterator priorityIt = priorities.iterator(); for (int i = 0; i < times; i++) { defaultPriority = (GenericValue) priorityIt.next(); } } if (defaultPriority == null) { throw new RuntimeException("Default priority not found"); } return defaultPriority.getString("id"); } /** * Given an array of addresses, this method returns the first valid * assignee for the appropriate project. * It returns null if addresses is null or empty array, or none of the * users found by addresses is assignable. * * @param addresses array of addresses * @param project project generic value * @return first assignable user based on the array of addresses */ public static User getFirstValidAssignee(Address[] addresses, GenericValue project) { if (addresses == null || addresses.length == 0) { return null; } for (Address address : addresses) { if (address instanceof InternetAddress) { InternetAddress email = (InternetAddress) address; try { User validUser = UserUtils.getUserByEmail(email.getAddress()); if (ManagerFactory.getPermissionManager().hasPermission(Permissions.ASSIGNABLE_USER, project, validUser)) { return validUser; } } catch (EntityNotFoundException e) { // keep cycling } } } return null; } private void setDefaultSecurityLevel(MutableIssue issue) throws Exception { GenericValue project = issue.getProject(); if (project != null) { IssueSecurityLevelManager issueSecurityLevelManager = ManagerFactory.getIssueSecurityLevelManager(); final Long levelId = issueSecurityLevelManager.getSchemeDefaultSecurityLevel(project); if (levelId != null) { issue.setSecurityLevel(issueSecurityLevelManager.getIssueSecurity(levelId)); } } } /** * Text parts are not attached but rather potentially form the source of issue text. * However text part attachments are kept providing they aint empty. * * @param part The part which will have a content type of text/plain to be tested. * @return Only returns true if the part is an attachment and not empty * @throws MessagingException * @throws IOException */ protected boolean attachPlainTextParts(final Part part) throws MessagingException, IOException { return !MailUtils.isContentEmpty(part) && MailUtils.isPartAttachment(part); } /** * Html parts are not attached but rather potentially form the source of issue text. * However html part attachments are kept providing they aint empty. * * @param part The part which will have a content type of text/html to be tested. * @return Only returns true if the part is an attachment and not empty * @throws MessagingException * @throws IOException */ protected boolean attachHtmlParts(final Part part) throws MessagingException, IOException { return !MailUtils.isContentEmpty(part) && MailUtils.isPartAttachment(part); } }