com.aurel.track.util.event.MailHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.aurel.track.util.event.MailHandler.java

Source

/**
 * Genji Scrum Tool and Issue Tracker
 * Copyright (C) 2015 Steinbeis GmbH & Co. KG Task Management Solutions
    
 * <a href="http://www.trackplus.com">Genji Scrum Tool</a>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/* $Id:$ */

package com.aurel.track.util.event;

import java.io.StringWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import com.aurel.track.Constants;
import com.aurel.track.admin.customize.notify.settings.NotifySettingsBL;
import com.aurel.track.beans.ILabelBean;
import com.aurel.track.beans.TMailTemplateBean;
import com.aurel.track.beans.TNotifySettingsBean;
import com.aurel.track.beans.TPersonBean;
import com.aurel.track.beans.TProjectBean;
import com.aurel.track.beans.TSiteBean;
import com.aurel.track.beans.TWorkItemBean;
import com.aurel.track.fieldType.runtime.base.FieldChange;
import com.aurel.track.fieldType.runtime.base.LookupContainer;
import com.aurel.track.item.ItemBL;
import com.aurel.track.item.SendItemEmailBL;
import com.aurel.track.prop.ApplicationBean;
import com.aurel.track.resources.LocalizeUtil;
import com.aurel.track.util.LocaleHandler;
import com.aurel.track.util.PropertiesHelper;
import com.aurel.track.util.emailHandling.Html2Text;
import com.aurel.track.util.emailHandling.JavaMailBean;
import com.aurel.track.util.emailHandling.MailBoxUsers;
import com.aurel.track.util.event.parameters.AfterItemSaveEventParam;

import freemarker.template.Template;

public abstract class MailHandler extends Thread {

    private static final Logger LOGGER = LogManager.getLogger(MailHandler.class);

    protected AfterItemSaveEventParam afterItemSaveEventParam;

    protected static JavaMailBean mailBean = JavaMailBean.getInstance();

    protected TWorkItemBean workItemBean = null;
    protected TWorkItemBean workItemBeanOld = null;
    protected Locale locale;
    protected boolean isCreated = false;
    protected Recipients recipients = null;
    protected TPersonBean changedByPerson = null;

    /**
     * Parameters in the root context. They can be referenced by ${<parameterName>} in e-mail subject or body
     * @author Tamas
     *
     */
    public static interface ROOT_PARAMETERS {
        /**
         * direct item fields
         */
        static String ITEM_NO = "itemNo";
        static String TITLE = "title";
        static String PROJECT = "project";
        static String ITEM_TYPE = "itemType";
        static String STATUS = "status";
        static String CHANGED_BY = "changedBy";
        static String SUBMITTER_EMAIL = "submitterEmail";
        /**
         * derived fields
         */
        static String SUBJECT = "subject";
        //this will be parsed at a possible replay
        static String MARKER = "marker";
        //explanation of what was changed
        static String CHANGE_DETAIL = "changeDetail";
        static String CHANGE_DETAIL_KEY = "changeDetailKey";
        //HTML link to the item
        static String MORE_INFO = "moreInfo";
    }

    public static interface MAIL_PART {
        public static final int SUBJECT = 1;
        public static final int BODY = 2;
    }

    /**
     * Gets the field change map for a specific locale
     * This can be reused for different users to be notified having the same locale
     * @param afterItemSaveEventParam
     * @param locale
     * @return
     */
    protected abstract SortedMap<Integer, FieldChange> getLocalizedFieldChangesMap(
            AfterItemSaveEventParam afterItemSaveEventParam, Locale locale);

    /**
     * Prepares the root context for freemarker template
     * @param personBean
     * @param personLocale
     * @param isPlain
     * @return
     */
    protected abstract Map<String, Object> getRootContext(TPersonBean personBean, Locale personLocale,
            boolean isPlain, Set<Integer> pickerRaciRolesSet,
            SortedMap<Integer, FieldChange> localizedFieldChangesMap);

    /**
     * Prepares the common variables in the root context
     * @param fieldChangesMap
     * @param locale
     * @return
     */
    protected Map<String, Object> prepareRootContext(SortedMap<Integer, FieldChange> fieldChangesMap,
            Locale locale) {
        Map<String, Object> rootContext = new HashMap<String, Object>();
        String itemID = ItemBL.getItemNo(workItemBean);
        rootContext.put(ROOT_PARAMETERS.ITEM_NO, itemID);
        rootContext.put(ROOT_PARAMETERS.TITLE, workItemBean.getSynopsis());
        String projectLabel = null;
        ILabelBean projectBean = LookupContainer.getProjectBean(workItemBean.getProjectID());
        if (projectBean != null) {
            projectLabel = projectBean.getLabel();
        }
        rootContext.put(ROOT_PARAMETERS.PROJECT, projectLabel);
        ILabelBean itemTypeBean = LookupContainer.getItemTypeBean(workItemBean.getListTypeID(), locale);
        if (itemTypeBean != null) {
            rootContext.put(ROOT_PARAMETERS.ITEM_TYPE, itemTypeBean.getLabel());
        }
        ILabelBean statusBean = LookupContainer.getStatusBean(workItemBean.getStateID(), locale);
        if (statusBean != null) {
            rootContext.put(ROOT_PARAMETERS.STATUS, statusBean.getLabel());
        }
        rootContext.put(ROOT_PARAMETERS.CHANGED_BY, getChangedByPerson().getPureFullName());
        rootContext.put(ROOT_PARAMETERS.SUBMITTER_EMAIL, workItemBean.getSubmitterEmail());
        String marker = SendItemEmailBL.getMarker(workItemBean, locale);
        rootContext.put(ROOT_PARAMETERS.MARKER, marker);
        String getChangeDetailKey = getChangeDetailKey();
        String changeDetail = null;
        if (getChangeDetailKey != null) {
            changeDetail = LocalizeUtil.getParametrizedString(getChangeDetailKey(),
                    getChangeDetailParameters(itemID, workItemBean, fieldChangesMap, locale), locale);
            rootContext.put(ROOT_PARAMETERS.CHANGE_DETAIL, changeDetail);
            rootContext.put(ROOT_PARAMETERS.CHANGE_DETAIL_KEY, getChangeDetailKey());
        }
        String subject = marker + "[" + projectLabel + "] " + changeDetail;
        rootContext.put(ROOT_PARAMETERS.SUBJECT, subject);
        rootContext.put(ROOT_PARAMETERS.MORE_INFO, Constants.Hyperlink + workItemBean.getObjectID());
        return rootContext;
    }

    /**
     * Get the resource key of the detailed explanation
     * @return
     */
    protected abstract String getChangeDetailKey();

    /**
     * Get the parameters of the parameterized detailed explanation
     * @param itemID
     * @param workItemBean
     * @param fieldChangesMap
     * @param locale
     * @return
     */
    protected abstract Object[] getChangeDetailParameters(String itemID, TWorkItemBean workItemBean,
            SortedMap<Integer, FieldChange> fieldChangesMap, Locale locale);

    /**
     * the workItem's projectID (both the old and the new projectID if project was changed)
     */
    protected List<Integer> projectIDs = new LinkedList<Integer>();

    protected MailHandler(TWorkItemBean workItemBean, TWorkItemBean workItemBeanOld, Locale locale) {
        this.workItemBean = workItemBean;
        this.workItemBeanOld = workItemBeanOld;
        this.locale = locale;
        projectIDs.add(workItemBean.getProjectID());
    }

    @Override
    public void run() {
        sendEntireMail();
    }

    /**
     * Gets the e-mail templatr
     * @return
     */
    protected abstract TMailTemplateBean getTemplate();

    /**
     * Gather the recipients
     */
    protected void gatherRecipients() {
        recipients = new Recipients(workItemBean, workItemBeanOld);
        recipients.buildRaciList(projectIDs, isCreated);
    }

    /**
     * Whether the email sending is really needed
     * @return
     */
    protected abstract boolean sendCondition();

    /**
     * The full name of the person who made the action
     * @return
     */
    protected abstract TPersonBean getChangedByPerson();

    /**
     * The project bean the modified issue belongs to
     * @return
     */
    protected abstract TProjectBean getProjectBean();

    public static Locale getLocaleForPerson(TPersonBean sendToPerson, TPersonBean changedByPersonBean,
            Locale detectedLocale) {
        if (sendToPerson != null && sendToPerson.getObjectID() == null && detectedLocale != null) {
            //for unknown user change to the detected language 
            return detectedLocale;
        }
        Locale personLocale = getLocaleForPerson(sendToPerson);
        if (personLocale == null) {
            Integer personID = sendToPerson.getObjectID();
            if (personID != null) {
                //if track+ registered person (not POP3 email submitter person)
                if (personLocale == null) {
                    //fall back to changer person's locale
                    personLocale = getLocaleForPerson(changedByPersonBean);
                }
            }
            if (personLocale == null) {
                //set it back to English
                personLocale = Locale.getDefault();
            }
        }
        return personLocale;
    }

    /**
     * Gets the locale set for a person
     * @param personBean
     * @return
     */
    private static Locale getLocaleForPerson(TPersonBean personBean) {
        if (personBean == null) {
            return null;
        }
        String localeString = personBean.getPrefLocale();
        if (localeString == null || "".equals(localeString.trim())) {
            return null;
        }
        return LocaleHandler.getLocaleFromString(localeString);
    }

    /**
     * This method finally sends the entire mail from this transaction.
     */
    public void sendEntireMail() {
        //when send condition is not satisfied do not send email
        if (!sendCondition()) {
            return;
        }
        //gather the recipients
        gatherRecipients();
        //get the sender-address
        TPersonBean sendFrom = getSendFrom(getProjectBean(), getChangedByPerson());
        TPersonBean replayToPerson = MailHandler.getReplayTo(getProjectBean(), getChangedByPerson());
        TMailTemplateBean mailTemplateBean = getTemplate();
        if (mailTemplateBean == null) {
            LOGGER.warn("No mail template found");
            return;
        }
        //all persons to notify through direct RACI roles
        List<TPersonBean> allDirectPersons = recipients.getPersonsBeansToNotify();
        //direct RACI persons to substitute persons
        Map<Integer, Integer> personToSubstituteMap = recipients.getPersonToSubstituteMap();
        Collection<Integer> allSubstituteIDs = personToSubstituteMap.values();
        //RACI persons to notify directly (only To:) who are not substitutes for other person
        List<TPersonBean> personsToNotifyDirectly = new LinkedList<TPersonBean>();
        //RACI persons to notify directly (only To:) who are at the same  time substitutes for other person
        List<TPersonBean> substitutePersonsToNotifyDirectly = new LinkedList<TPersonBean>();
        Set<Integer> substitutesIDsToNotifyDirectly = new HashSet<Integer>();
        //split direct RACI persons to those who are also substitute at the same time for other direct RACI person, and those who are not substitutes
        for (TPersonBean personBean : allDirectPersons) {
            Integer personID = personBean.getObjectID();
            if (allSubstituteIDs.contains(personID)) {
                LOGGER.debug("Person to be notified directly " + personBean.getLabel() + "(" + personID
                        + ") is also substitute person");
                substitutePersonsToNotifyDirectly.add(personBean);
                substitutesIDsToNotifyDirectly.add(personID);
            } else {
                personsToNotifyDirectly.add(personBean);
            }
        }
        //get those substitutes who are not RACI persons for item and put them in the Cc list
        Set<Integer> substitutesIDsToNotifyByCc = new HashSet<Integer>();
        if (allSubstituteIDs != null && !allSubstituteIDs.isEmpty()) {
            for (Integer substituteID : allSubstituteIDs) {
                if (!substitutesIDsToNotifyDirectly.contains(substituteID)) {
                    LOGGER.debug("Substitute person " + substituteID + " added for Cc");
                    substitutesIDsToNotifyByCc.add(substituteID);
                }
            }
        }
        Set<String> mailboxUsersSet = MailBoxUsers.getMailboxUsersSet();
        Map<Locale, SortedMap<Integer, FieldChange>> localizedFieldChangesMapForLocale = new HashMap<Locale, SortedMap<Integer, FieldChange>>();
        //send e-mails to those RACI persons who are at the same time substitutes for other RACI person
        if (substitutesIDsToNotifyDirectly != null && !substitutesIDsToNotifyDirectly.isEmpty()) {
            for (TPersonBean personBean : substitutePersonsToNotifyDirectly) {
                String emailTo = personBean.getEmail();
                if (emailTo != null && mailboxUsersSet != null && mailboxUsersSet.contains(emailTo)) {
                    LOGGER.error("The e-mail address " + emailTo + " set for person " + personBean.getName()
                            + " is used a mailbox for email submission. The notification e-mail will not be sent to avoid infinite cycle of e-mailing");
                    continue;
                }
                Integer personID = personBean.getObjectID();
                Locale personLocale = getLocaleForPerson(personBean, sendFrom, locale);
                SortedMap<Integer, FieldChange> localizedFieldChangesMap = localizedFieldChangesMapForLocale
                        .get(personLocale);
                if (localizedFieldChangesMap == null) {
                    localizedFieldChangesMap = getLocalizedFieldChangesMap(afterItemSaveEventParam, personLocale);
                    localizedFieldChangesMapForLocale.put(personLocale, localizedFieldChangesMap);
                }
                boolean plainEmail = personBean.isPreferredEmailTypePlain();
                Set<Integer> pickerRoles = recipients.getPickerRaciRolesForPerson(personID);
                Map<String, Object> root = getRootContext(personBean, personLocale, plainEmail, pickerRoles,
                        localizedFieldChangesMap);

                boolean emailSent = sendEmailOnPerson(personBean, personLocale, sendFrom, null, replayToPerson,
                        mailTemplateBean.getObjectID(), root);
                if (emailSent) {
                    LOGGER.debug("E-mail sent to the subtitute person to be notified directly "
                            + personBean.getLabel() + "(" + personID + ")");
                } else {
                    //sending the e-mail was not successful, add it to the Cc list
                    LOGGER.debug("E-mail not  sent to the subtitute person to be notified directly "
                            + personBean.getLabel() + "(" + personID + "). Added as Cc");
                    substitutesIDsToNotifyByCc.add(personBean.getObjectID());
                }
            }
        }

        for (TPersonBean personBean : personsToNotifyDirectly) {
            String emailTo = personBean.getEmail();
            if (emailTo != null && mailboxUsersSet != null && mailboxUsersSet.contains(emailTo)) {
                LOGGER.error("The e-mail address " + emailTo + " set for person " + personBean.getName()
                        + " is used a mailbox for email submission. The notification e-mail will not be sent to avoid infinite cycle of e-mailing");
                continue;
            }
            Integer personID = personBean.getObjectID();
            Integer substituteID = null;
            TPersonBean ccPerson = null;
            if (personToSubstituteMap != null) {
                substituteID = personToSubstituteMap.get(personID);
                if (substituteID != null && substitutesIDsToNotifyByCc.contains(substituteID)) {
                    ccPerson = LookupContainer.getPersonBean(substituteID);
                }
            }
            Locale personLocale = getLocaleForPerson(personBean, sendFrom, locale);
            SortedMap<Integer, FieldChange> localizedFieldChangesMap = localizedFieldChangesMapForLocale
                    .get(personLocale);
            if (localizedFieldChangesMap == null) {
                localizedFieldChangesMap = getLocalizedFieldChangesMap(afterItemSaveEventParam, personLocale);
                localizedFieldChangesMapForLocale.put(personLocale, localizedFieldChangesMap);
            }
            boolean plainEmail = personBean.isPreferredEmailTypePlain();
            Set<Integer> pickerRoles = recipients.getPickerRaciRolesForPerson(personID);
            Map<String, Object> root = getRootContext(personBean, personLocale, plainEmail, pickerRoles,
                    localizedFieldChangesMap);
            boolean emailSent = sendEmailOnPerson(personBean, personLocale, sendFrom, ccPerson, replayToPerson,
                    mailTemplateBean.getObjectID(), root);
            if (emailSent) {
                if (substituteID != null) {
                    //if the person is substitute for two different RACI persons then add for only for one of them into Cc (to not receive two or more e-mails as Cc)
                    substitutesIDsToNotifyByCc.remove(substituteID);
                }
            }
        }
    }

    /**
     * Send e-mail to a person
     * @param sendToPerson
     * @param personLocale
     * @param sendFromPerson
     * @param ccPerson
     * @param replayToPerson
     * @param mailTemplateID
     * @param root
     * @return
     */
    public static boolean sendEmailOnPerson(TPersonBean sendToPerson, Locale personLocale,
            TPersonBean sendFromPerson, TPersonBean ccPerson, TPersonBean replayToPerson, Integer mailTemplateID,
            Map<String, Object> root) {
        boolean plainEmail = sendToPerson.isPreferredEmailTypePlain();
        Integer personID = sendToPerson.getObjectID();
        MailPartTemplates mailPartTemplates = EmailTemplatesContainer.getMailPartTemplates(mailTemplateID,
                plainEmail, personLocale);
        if (mailPartTemplates == null) {
            LOGGER.warn("No mail template found for mailTemplateID " + mailTemplateID + " plain " + plainEmail
                    + " locale " + personLocale);
            return false;
        }
        Template subjectTemplate = mailPartTemplates.getSubjectTemplate();
        if (subjectTemplate == null) {
            if (plainEmail) {
                LOGGER.warn("No plain subject template found for person " + sendToPerson.getObjectID());
            } else {
                LOGGER.warn("No HTML subject template found for person " + sendToPerson.getObjectID());
            }
            return false;
        }
        Template bodyTemplate = mailPartTemplates.getBodyTemplate();
        if (bodyTemplate == null) {
            if (plainEmail) {
                LOGGER.warn("No plain body template found for person " + sendToPerson.getObjectID() + ": "
                        + sendToPerson.getFullName() + ": " + personLocale);
            } else {
                LOGGER.warn("No HTML body template found for person " + sendToPerson.getObjectID() + ": "
                        + sendToPerson.getFullName() + ": " + personLocale);
            }
            return false;
        }
        if (root == null) {
            //do not send mail (no notification needed)
            LOGGER.debug("Root context is null for person " + sendToPerson.getName() + " (" + personID + ")");
            return false;
        }
        StringWriter subjectWriter = new StringWriter();
        try {
            LOGGER.debug("Processing the subject template...");
            subjectTemplate.process(root, subjectWriter);
            LOGGER.debug("Subject template processed.");
        } catch (Exception e) {
            LOGGER.warn(
                    "Processing the subject template " + bodyTemplate.getName() + " failed with " + e.getMessage());
            LOGGER.debug("Processed template: " + subjectWriter.toString());
            return false;
        }
        subjectWriter.flush();
        String messageSubject = subjectWriter.toString();
        StringWriter bodyWriter = new StringWriter();
        try {
            LOGGER.debug("Processing the body template...");
            bodyTemplate.process(root, bodyWriter);
            LOGGER.debug("Body template processed.");
        } catch (Exception e) {
            LOGGER.warn(
                    "Processing the body template " + bodyTemplate.getName() + " failed with " + e.getMessage());
            LOGGER.debug("Processed template: " + bodyWriter.toString());
            return false;
        }
        bodyWriter.flush();
        String messageBody = bodyWriter.toString();
        String emailTo = sendToPerson.getEmail();
        //Assemble and send the mail
        try {
            LOGGER.debug("Just before sending...");
            LOGGER.debug("From: " + sendFromPerson.getLabel() + " " + sendFromPerson.getEmail());
            LOGGER.debug("To: " + emailTo);
            LOGGER.debug("Subject: " + messageSubject);
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Body " + messageBody);
            }
            LOGGER.debug("Is plain: " + plainEmail);
            mailBean.sendMailInThread(sendToPerson, messageSubject, sendFromPerson, ccPerson, replayToPerson,
                    messageBody, plainEmail);
            LOGGER.debug("Mail sent to " + emailTo);
            return true;
        } catch (Exception e) { // SMTPException
            // add code to propagate Email problems to user interface or
            // system log
            LOGGER.error("Sending the e-mail failed with " + e.getMessage());
            LOGGER.debug(ExceptionUtils.getStackTrace(e));
            return false;
        }
    }

    /**
     * Gets the sender text
     * @param projectBean
     * @param changedByPerson
     * @return
     */
    public static TPersonBean getSendFrom(TProjectBean projectBean, TPersonBean changedByPerson) {
        if (projectBean != null) {
            String projectTrackSystemEmail = PropertiesHelper.getProperty(projectBean.getMoreProperties(),
                    TProjectBean.MOREPPROPS.TRACK_EMAIL_PROPERTY);
            String projectEmailPersonName = PropertiesHelper.getProperty(projectBean.getMoreProperties(),
                    TProjectBean.MOREPPROPS.EMAIL_PERSONAL_NAME);
            boolean useTrackFromAddress = "true".equalsIgnoreCase(PropertiesHelper.getProperty(
                    projectBean.getMoreProperties(), TProjectBean.MOREPPROPS.USE_TRACK_FROM_ADDRESS_DISPLAY));

            if (useTrackFromAddress && projectTrackSystemEmail != null && projectTrackSystemEmail.length() > 0) {
                boolean useTrackFromAddressAsReplay = "true".equalsIgnoreCase(PropertiesHelper.getProperty(
                        projectBean.getMoreProperties(), TProjectBean.MOREPPROPS.USE_TRACK_FROM_AS_REPLAY_TO));
                if (useTrackFromAddressAsReplay == false) {
                    //verify address
                    try {
                        InternetAddress tmp = new InternetAddress(projectTrackSystemEmail);
                        LOGGER.info(tmp.getAddress() + " is valid!");
                        TPersonBean sendFrom = new TPersonBean("", projectEmailPersonName, projectTrackSystemEmail);
                        return sendFrom;
                    } catch (AddressException e) {
                        LOGGER.error("The email address:'" + projectTrackSystemEmail + "' set in project"
                                + projectBean.getLabel() + " is invalid!");
                    }
                }
            }
        }
        String emailAddress = ApplicationBean.getInstance().getSiteBean().getSendFrom().getEmail();
        TPersonBean sendFromPersonbean;
        if (ApplicationBean.getInstance().getSiteBean().getUseTrackFromAddressDisplay()
                .equals(TSiteBean.SEND_FROM_MODE.SYSTEM)) {
            //use the track-email address as sender address
            sendFromPersonbean = ApplicationBean.getInstance().getSiteBean().getSendFrom();
            sendFromPersonbean.setEmail(emailAddress);
        } else {
            // we use the one who changed the item as sender address
            sendFromPersonbean = new TPersonBean(changedByPerson.getFirstName(), changedByPerson.getLastName(),
                    changedByPerson.getEmail());
            if (sendFromPersonbean.getLastName() == null) {
                sendFromPersonbean.setLastName("");
            }
            sendFromPersonbean.setLastName(
                    sendFromPersonbean.getLastName() + ApplicationBean.getInstance().getSiteBean().getTrackEmail());
        }
        LOGGER.debug("send from:" + sendFromPersonbean.getFullName() + ": " + sendFromPersonbean.getEmail());
        return sendFromPersonbean;
    }

    public static TPersonBean getReplayTo(TProjectBean projectBean, TPersonBean changedByPerson) {
        if (projectBean != null) {
            String projectTrackSystemEmail = PropertiesHelper.getProperty(projectBean.getMoreProperties(),
                    TProjectBean.MOREPPROPS.TRACK_EMAIL_PROPERTY);
            String projectEmailPersonName = PropertiesHelper.getProperty(projectBean.getMoreProperties(),
                    TProjectBean.MOREPPROPS.EMAIL_PERSONAL_NAME);
            boolean useTrackFromAddressDisplay = "true".equalsIgnoreCase(PropertiesHelper.getProperty(
                    projectBean.getMoreProperties(), TProjectBean.MOREPPROPS.USE_TRACK_FROM_ADDRESS_DISPLAY));

            if (useTrackFromAddressDisplay && projectTrackSystemEmail != null
                    && projectTrackSystemEmail.length() > 0) {
                boolean useTrackFromAddressAsReplay = "true".equalsIgnoreCase(PropertiesHelper.getProperty(
                        projectBean.getMoreProperties(), TProjectBean.MOREPPROPS.USE_TRACK_FROM_AS_REPLAY_TO));
                if (useTrackFromAddressAsReplay) {
                    //verify address
                    try {
                        InternetAddress tmp = new InternetAddress(projectTrackSystemEmail);
                        LOGGER.info(tmp.getAddress() + " is valid!");
                        TPersonBean sendFrom = new TPersonBean("", projectEmailPersonName, projectTrackSystemEmail);
                        return sendFrom;
                    } catch (AddressException e) {
                        LOGGER.error("The email address:'" + projectTrackSystemEmail + "' set in project"
                                + projectBean.getLabel() + " is invalid!");
                    }
                }
            }
        }
        return null;
    }

    /**
     * Converts the HTML long text to plain text
     * @param text
     * @return
     */
    protected static String convertHTML2Text(String text) {
        if (text == null) {
            return "";
        }
        try {
            return Html2Text.getNewInstance().convert(text);
        } catch (Exception e) {
            LOGGER.warn("Converting the HTML to plain text failed with " + e.getMessage());
            return text;
        }
    }

    /**
     * Set the localized change detail strings depending on the operation (add/change/remove)
     * and on field length (short/long field, where long field can be description or coomment)
     * @param fieldChange
     * @param longFields
     * @param locale
     * @param isNew whether creating a new issue (isCreate || isCopy) or editing an existing one
     */
    protected static void setLocalizedChangeDetail(FieldChange fieldChange, List<Integer> longFields, Locale locale,
            boolean isNew) {
        //if fieldID==null for example the budget/expense description has no explicit fieldID: in this case it is considered longField
        Integer fieldID = fieldChange.getFieldID();
        String fieldLabel = fieldChange.getLocalizedFieldLabel();
        String newValue = fieldChange.getNewShowValue();
        String oldValue = fieldChange.getOldShowValue();
        Object[] msgArguments;
        if (newValue == null) {
            newValue = "";
        }
        if (oldValue == null) {
            oldValue = "";
        }
        if (!newValue.equals(oldValue)) {
            if ("".equals(oldValue)) {
                //new value (by new issue, existing issue or by copy)
                if (fieldID == null || longFields.contains(fieldID)) {
                    //long value added (description/comment/attachment)
                    if (isNew) {
                        //if new issue write only "Description" not "Description was added"
                        fieldChange.setLocalizedChangeDetail(fieldLabel);
                    } else {
                        msgArguments = new Object[] { fieldLabel };
                        fieldChange.setLocalizedChangeDetail(LocalizeUtil
                                .getParametrizedString("item.history.wasAddedLong", msgArguments, locale));
                    }
                } else {
                    //short value added, used only in plain e-mails
                    msgArguments = new Object[] { fieldLabel, newValue };
                    fieldChange.setLocalizedChangeDetail(
                            LocalizeUtil.getParametrizedString("item.history.wasAddedShort", msgArguments, locale));
                }
            } else {
                if ("".equals(newValue)) {
                    //remove value (only by existing issue or by copy)
                    if (fieldID == null || longFields.contains(fieldID)) {
                        //long value removed (description or comment)
                        msgArguments = new Object[] { fieldLabel };
                        fieldChange.setLocalizedChangeDetail(LocalizeUtil
                                .getParametrizedString("item.history.wasRemovedLong", msgArguments, locale));
                    } else {
                        //short value removed, used only in plain e-mails
                        msgArguments = new Object[] { fieldLabel, oldValue };
                        fieldChange.setLocalizedChangeDetail(LocalizeUtil
                                .getParametrizedString("item.history.wasRemovedShort", msgArguments, locale));
                    }
                } else {
                    //change value (only by existing issue or by copy)
                    if (fieldID == null || longFields.contains(fieldID)) {
                        //long value changed (description or comment)
                        msgArguments = new Object[] { fieldLabel };
                        fieldChange.setLocalizedChangeDetail(LocalizeUtil
                                .getParametrizedString("item.history.wasChangedLong", msgArguments, locale));
                    } else {
                        //short value changed, used only in plain e-mails
                        msgArguments = new Object[] { fieldLabel, oldValue, newValue };
                        fieldChange.setLocalizedChangeDetail(LocalizeUtil
                                .getParametrizedString("item.history.wasChangedShort", msgArguments, locale));
                    }
                }
            }
        }
    }

    /**
     * Gets the project's or the nearest ancestor project's notify settings
     * @param personID for this person if specified, otherwise the default project settings of the project or the nearest ancestor project
     * @param projectID
     * @return
     */
    protected static TNotifySettingsBean getNearestNotifySettings(Integer personID, Integer projectID) {
        while (projectID != null) {
            TNotifySettingsBean notifySettingsBean = null;
            if (personID == null) {
                notifySettingsBean = NotifySettingsBL.loadDefaultByProject(projectID);
            } else {
                notifySettingsBean = NotifySettingsBL.loadOwnByPersonAndProject(personID, projectID);
            }
            if (notifySettingsBean != null) {
                LOGGER.debug("Notify setting found for projectID " + projectID + " and person "
                        + (personID == null ? "no person" : personID));
                return notifySettingsBean;
            } else {
                TProjectBean projectBean = LookupContainer.getProjectBean(projectID);
                if (projectBean == null) {
                    projectID = null;
                    break;
                } else {
                    projectID = ((TProjectBean) projectBean).getParent();
                }
            }
        }
        return null;
    }
}