/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/

package org.apache.ofbiz.party.communication;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.ofbiz.base.location.FlexibleLocation;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.GeneralException;
import org.apache.ofbiz.base.util.StringUtil;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilHttp;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.common.email.NotificationServices;
import org.apache.ofbiz.content.data.DataResourceWorker;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.condition.EntityOperator;
import org.apache.ofbiz.entity.util.EntityListIterator;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.entity.util.EntityUtil;
import org.apache.ofbiz.entity.util.EntityUtilProperties;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.service.mail.MimeMessageWrapper;

public class CommunicationEventServices {

    public static final String module = CommunicationEventServices.class.getName();
    public static final String resource = "PartyErrorUiLabels";

    public static Map<String, Object> sendCommEventAsEmail(DispatchContext ctx, Map<String, ? extends Object> context) {
        Delegator delegator = ctx.getDelegator();
        LocalDispatcher dispatcher = ctx.getDispatcher();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        Locale locale = (Locale) context.get("locale");

        String communicationEventId = (String) context.get("communicationEventId");

        Map<String, Object> result = ServiceUtil.returnSuccess();
        List<Object> errorMessages = new LinkedList<>(); // used to keep a list of all error messages returned from sending emails to contact list

        try {
            // find the communication event and make sure that it is actually an email
            GenericValue communicationEvent = EntityQuery.use(delegator).from("CommunicationEvent").where("communicationEventId", communicationEventId).queryOne();
            if (communicationEvent == null) {
                String errMsg = UtilProperties.getMessage(resource,"commeventservices.communication_event_not_found_failure", locale);
                return ServiceUtil.returnError(errMsg + " " + communicationEventId);
            }
            String communicationEventType = communicationEvent.getString("communicationEventTypeId");
            if (communicationEventType == null || !("EMAIL_COMMUNICATION".equals(communicationEventType) || "AUTO_EMAIL_COMM".equals(communicationEventType))) {
                String errMsg = UtilProperties.getMessage(resource,"commeventservices.communication_event_must_be_email_for_email", locale);
                return ServiceUtil.returnError(errMsg + " " + communicationEventId);
            }

            // make sure the from contact mech is an email if it is specified
            if ((communicationEvent.getRelatedOne("FromContactMech", false) == null) ||
                 (!("EMAIL_ADDRESS".equals(communicationEvent.getRelatedOne("FromContactMech", false).getString("contactMechTypeId"))) ||
                 (communicationEvent.getRelatedOne("FromContactMech", false).getString("infoString") == null))) {
                String errMsg = UtilProperties.getMessage(resource,"commeventservices.communication_event_from_contact_mech_must_be_email", locale);
                return ServiceUtil.returnError(errMsg + " " + communicationEventId);
            }

            // assign some default values because required by sendmail and better not make them defaults over there
            if (UtilValidate.isEmpty(communicationEvent.getString("subject"))) {
                communicationEvent.put("subject", " ");
            }
            if (UtilValidate.isEmpty(communicationEvent.getString("content"))) {
                communicationEvent.put("content", " ");
            }

            // prepare the email
            Map<String, Object> sendMailParams = new HashMap<>();
            sendMailParams.put("sendFrom", communicationEvent.getRelatedOne("FromContactMech", false).getString("infoString"));
            sendMailParams.put("subject", communicationEvent.getString("subject"));
            sendMailParams.put("contentType", communicationEvent.getString("contentMimeTypeId"));
            sendMailParams.put("userLogin", userLogin);

            Debug.logInfo("Sending communicationEvent: " + communicationEventId, module);

            // check for attachments
            boolean isMultiPart = false;
            List <GenericValue> comEventContents = EntityQuery.use(delegator).from("CommEventContentAssoc").where("communicationEventId", communicationEventId).filterByDate().queryList();
            if (UtilValidate.isNotEmpty(comEventContents)) {
                isMultiPart = true;
                List<Map<String, ? extends Object>> bodyParts = new LinkedList<>();
                if (UtilValidate.isNotEmpty(communicationEvent.getString("content"))) {
                    bodyParts.add(UtilMisc.<String, Object>toMap("content", communicationEvent.getString("content"), "type", communicationEvent.getString("contentMimeTypeId")));
                }
                for (GenericValue comEventContent : comEventContents) {
                    GenericValue content = comEventContent.getRelatedOne("FromContent", false);
                    GenericValue dataResource = content.getRelatedOne("DataResource", false);
                    ByteBuffer dataContent = DataResourceWorker.getContentAsByteBuffer(delegator, dataResource.getString("dataResourceId"), null, null, locale, null);
                    bodyParts.add(UtilMisc.<String, Object>toMap("content", dataContent.array(), "type", dataResource.getString("mimeTypeId"), "filename", dataResource.getString("dataResourceName")));
                }
                sendMailParams.put("bodyParts", bodyParts);
            } else {
                sendMailParams.put("body", communicationEvent.getString("content"));
            }

            // if there is no contact list, then send look for a contactMechIdTo and partyId
            if ((UtilValidate.isEmpty(communicationEvent.getString("contactListId")))) {
                // send to address
                String sendTo = communicationEvent.getString("toString");

                if (UtilValidate.isEmpty(sendTo)) {
                    GenericValue toContactMech = communicationEvent.getRelatedOne("ToContactMech", false);
                    if (toContactMech != null && "EMAIL_ADDRESS".equals(toContactMech.getString("contactMechTypeId"))) {
                        sendTo = toContactMech.getString("infoString");
                    }
                }
                if (UtilValidate.isEmpty(sendTo)) {
                    String errMsg = UtilProperties.getMessage(resource,"commeventservices.communication_event_to_contact_mech_must_be_email", locale);
                    return ServiceUtil.returnError(errMsg + " " + communicationEventId);
                }

                // add other parties from roles
                String sendCc = null;
                String sendBcc = null;
                List <GenericValue> commRoles = communicationEvent.getRelated("CommunicationEventRole", null, null, false);
                if (UtilValidate.isNotEmpty(commRoles)) {
                    for (GenericValue commRole : commRoles) { // 'from' and 'to' already defined on communication event
                        if (commRole.getString("partyId").equals(communicationEvent.getString("partyIdFrom")) || commRole.getString("partyId").equals(communicationEvent.getString("partyIdTo"))) {
                            continue;
                        }
                        GenericValue contactMech = commRole.getRelatedOne("ContactMech", false);
                        if (contactMech != null && UtilValidate.isNotEmpty(contactMech.getString("infoString"))) {
                            if ("ADDRESSEE".equals(commRole.getString("roleTypeId"))) {
                                sendTo += "," + contactMech.getString("infoString");
                            } else if ("CC".equals(commRole.getString("roleTypeId"))) {
                                if (sendCc != null) {
                                    sendCc += "," + contactMech.getString("infoString");
                                } else {
                                    sendCc = contactMech.getString("infoString");
                                }
                            } else if ("BCC".equals(commRole.getString("roleTypeId"))) {
                                if (sendBcc != null) {
                                    sendBcc += "," + contactMech.getString("infoString");
                                } else {
                                    sendBcc = contactMech.getString("infoString");
                                }
                            }
                        }
                    }
                }

                sendMailParams.put("communicationEventId", communicationEventId);
                sendMailParams.put("sendTo", sendTo);
                if (sendCc != null) {
                    sendMailParams.put("sendCc", sendCc);
                }
                if (sendBcc != null) {
                    sendMailParams.put("sendBcc", sendBcc);
                }
                sendMailParams.put("partyId", communicationEvent.getString("partyIdTo"));  // who it's going to

                // send it - using a new transaction
                Map<String, Object> tmpResult = null;
                if (isMultiPart) {
                    tmpResult = dispatcher.runSync("sendMailMultiPart", sendMailParams, 360, true);
                } else {
                    tmpResult = dispatcher.runSync("sendMail", sendMailParams, 360, true);
                }

                if (ServiceUtil.isError(tmpResult)) {
                    if (ServiceUtil.getErrorMessage(tmpResult).startsWith("[ADDRERR]")) {
                        // address error; mark the communication event as BOUNCED
                        communicationEvent.set("statusId", "COM_BOUNCED");
                        try {
                            communicationEvent.store();
                        } catch (GenericEntityException e) {
                            Debug.logError(e, module);
                            return ServiceUtil.returnError(e.getMessage());
                        }
                    } else {
                        // setup or communication error
                        errorMessages.add(ServiceUtil.getErrorMessage(tmpResult));
                    }
                } else {
                    // set the message ID on this communication event
                    String messageId = (String) tmpResult.get("messageId");
                    communicationEvent.set("messageId", messageId);
                    try {
                        communicationEvent.store();
                    } catch (GenericEntityException e) {
                        Debug.logError(e, module);
                        return ServiceUtil.returnError(e.getMessage());
                    }

                    Map<String, Object> completeResult = dispatcher.runSync("setCommEventComplete", UtilMisc.<String, Object>toMap("communicationEventId", communicationEventId, "partyIdFrom", communicationEvent.getString("partyIdFrom"), "userLogin", userLogin));
                    if (ServiceUtil.isError(completeResult)) {
                        errorMessages.add(ServiceUtil.getErrorMessage(completeResult));
                    }
                }

            } else {
                // Call the sendEmailToContactList service if there's a contactListId present
                Map<String, Object> sendEmailToContactListContext = new HashMap<>();
                sendEmailToContactListContext.put("contactListId", communicationEvent.getString("contactListId"));
                sendEmailToContactListContext.put("communicationEventId", communicationEventId);
                sendEmailToContactListContext.put("userLogin", userLogin);
                try {
                    dispatcher.runAsync("sendEmailToContactList", sendEmailToContactListContext);
                } catch (GenericServiceException e) {
                    String errMsg = UtilProperties.getMessage(resource, "commeventservices.errorCallingSendEmailToContactListService", locale);
                    Debug.logError(e, errMsg, module);
                    errorMessages.add(errMsg);
                    errorMessages.addAll(e.getMessageList());
                }
            }
        } catch (IOException | GeneralException eey) {
            return ServiceUtil.returnError(eey.getMessage());
        }

        // If there were errors, then the result of this service should be error with the full list of messages
        if (errorMessages.size() > 0) {
            result = ServiceUtil.returnError(errorMessages);
        }
        return result;
    }

    public static Map<String, Object> sendEmailToContactList(DispatchContext ctx, Map<String, ? extends Object> context) {
        Delegator delegator = ctx.getDelegator();
        LocalDispatcher dispatcher = ctx.getDispatcher();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        Locale locale = (Locale) context.get("locale");

        List<Object> errorMessages = new LinkedList<>();
        String errorCallingUpdateContactListPartyService = UtilProperties.getMessage(resource, "commeventservices.errorCallingUpdateContactListPartyService", locale);
        String errorCallingSendMailService = UtilProperties.getMessage(resource, "commeventservices.errorCallingSendMailService", locale);
        String errorInSendEmailToContactListService = UtilProperties.getMessage(resource, "commeventservices.errorInSendEmailToContactListService", locale);
        String skippingInvalidEmailAddress = UtilProperties.getMessage(resource, "commeventservices.skippingInvalidEmailAddress", locale);

        String contactListId = (String) context.get("contactListId");
        String communicationEventId = (String) context.get("communicationEventId");

        // Any exceptions thrown in this block will cause the service to return error
        try {
            GenericValue communicationEvent = EntityQuery.use(delegator).from("CommunicationEvent").where("communicationEventId", communicationEventId).queryOne();
            GenericValue contactList = EntityQuery.use(delegator).from("ContactList").where("contactListId", contactListId).queryOne();

            Map<String, Object> sendMailParams = new HashMap<>();
            sendMailParams.put("sendFrom", communicationEvent.getRelatedOne("FromContactMech", false).getString("infoString"));
            sendMailParams.put("subject", communicationEvent.getString("subject"));
            sendMailParams.put("contentType", communicationEvent.getString("contentMimeTypeId"));
            sendMailParams.put("userLogin", userLogin);

            // Find a list of distinct email addresses from active, ACCEPTED parties in the contact list
            //      using a list iterator (because there can be a large number)
            List<EntityCondition> conditionList = UtilMisc.toList(
                        EntityCondition.makeCondition("contactListId", EntityOperator.EQUALS, contactList.get("contactListId")),
                        EntityCondition.makeCondition("statusId", EntityOperator.EQUALS, "CLPT_ACCEPTED"),
                        EntityCondition.makeCondition("preferredContactMechId", EntityOperator.NOT_EQUAL, null),
                        EntityUtil.getFilterByDateExpr(), EntityUtil.getFilterByDateExpr("contactFromDate", "contactThruDate"));

            EntityQuery eq = EntityQuery.use(delegator).select("partyId", "preferredContactMechId", "fromDate", "infoString")
                    .from("ContactListPartyAndContactMech")
                    .where(EntityCondition.makeCondition(conditionList, EntityOperator.AND))
                    .cursorScrollInsensitive()
                    .distinct();


            try (EntityListIterator eli = eq.queryIterator()) {
                // Send an email to each contact list member
                // loop through the list iterator
                for (GenericValue contactListPartyAndContactMech; (contactListPartyAndContactMech = eli.next()) != null;) {
                    Debug.logInfo("Contact info: " + contactListPartyAndContactMech, module);
                    // Any exceptions thrown in this inner block will only relate to a single email of the list, so should
                    //  only be logged and not cause the service to return an error
                    try {

                        String emailAddress = contactListPartyAndContactMech.getString("infoString");
                        if (UtilValidate.isEmpty(emailAddress)) {
                            continue;
                        }
                        emailAddress = emailAddress.trim();

                        if (! UtilValidate.isEmail(emailAddress)) {

                            // If validation fails, just log and skip the email address
                            Debug.logError(skippingInvalidEmailAddress + ": " + emailAddress, module);
                            errorMessages.add(skippingInvalidEmailAddress + ": " + emailAddress);
                            continue;
                        }

                        // Because we're retrieving infoString only above (so as not to pollute the distinctness), we
                        //      need to retrieve the partyId it's related to. Since this could be multiple parties, get
                        //      only the most recent valid one via ContactListPartyAndContactMech.
                        List<EntityCondition> clpConditionList = UtilMisc.makeListWritable(conditionList);
                        clpConditionList.add(EntityCondition.makeCondition("infoString", EntityOperator.EQUALS, emailAddress));

                        GenericValue lastContactListPartyACM = EntityQuery.use(delegator).from("ContactListPartyAndContactMech")
                                .where(EntityCondition.makeCondition(clpConditionList, EntityOperator.AND))
                                .orderBy("-fromDate")
                                .cache(true)
                                .queryFirst();
                        if (lastContactListPartyACM == null) {
                            continue;
                        }

                        String partyId = lastContactListPartyACM.getString("partyId");

                        sendMailParams.put("sendTo", emailAddress);
                        sendMailParams.put("partyId", partyId);

                        // Retrieve a record for this contactMechId from ContactListCommStatus
                        Map<String, String> contactListCommStatusRecordMap = UtilMisc.toMap("contactListId", contactListId, "communicationEventId", communicationEventId, "contactMechId", lastContactListPartyACM.getString("preferredContactMechId"));
                        GenericValue contactListCommStatusRecord = EntityQuery.use(delegator).from("ContactListCommStatus")
                                .where(contactListCommStatusRecordMap)
                                .queryOne();
                        if (contactListCommStatusRecord == null) {

                            // No attempt has been made previously to send to this address, so create a record to reflect
                            //  the beginning of the current attempt
                            Map<String, String> newContactListCommStatusRecordMap = UtilMisc.makeMapWritable(contactListCommStatusRecordMap);
                            newContactListCommStatusRecordMap.put("statusId", "COM_IN_PROGRESS");
                            newContactListCommStatusRecordMap.put("partyId", partyId);
                            contactListCommStatusRecord = delegator.create("ContactListCommStatus", newContactListCommStatusRecordMap);
                        } else if (contactListCommStatusRecord.get("statusId") != null && "COM_COMPLETE".equals(contactListCommStatusRecord.getString("statusId"))) {

                            // There was a successful earlier attempt, so skip this address
                            continue;
                        }

                        // Send e-mail
                        Debug.logInfo("Sending email to contact list [" + contactListId + "] party [" + partyId + "] : " + emailAddress, module);
                        // Make the attempt to send the email to the address

                        Map<String, Object> tmpResult = null;

                        // Retrieve a contact list party status
                        GenericValue contactListPartyStatus = EntityQuery.use(delegator).from("ContactListPartyStatus")
                                .where("contactListId", contactListId, "partyId", contactListPartyAndContactMech.getString("partyId"), "fromDate", contactListPartyAndContactMech.getTimestamp("fromDate"), "statusId", "CLPT_ACCEPTED")
                                .queryFirst();
                        if (contactListPartyStatus != null) {
                            // prepare body parameters
                            Map<String, Object> bodyParameters = new HashMap<>();
                            bodyParameters.put("contactListId", contactListId);
                            bodyParameters.put("partyId", contactListPartyAndContactMech.getString("partyId"));
                            bodyParameters.put("preferredContactMechId", contactListPartyAndContactMech.getString("preferredContactMechId"));
                            bodyParameters.put("emailAddress", emailAddress);
                            bodyParameters.put("fromDate", contactListPartyAndContactMech.getTimestamp("fromDate"));
                            bodyParameters.put("optInVerifyCode", contactListPartyStatus.getString("optInVerifyCode"));
                            bodyParameters.put("content", communicationEvent.getString("content"));
                            NotificationServices.setBaseUrl(delegator, contactList.getString("verifyEmailWebSiteId"), bodyParameters);

                            GenericValue webSite = EntityQuery.use(delegator).from("WebSite").where("webSiteId", contactList.getString("verifyEmailWebSiteId")).queryOne();
                            if (webSite != null) {
                                GenericValue productStore = webSite.getRelatedOne("ProductStore", false);
                                if (productStore != null) {
                                    List<GenericValue> productStoreEmailSettings = productStore.getRelated("ProductStoreEmailSetting", UtilMisc.toMap("emailType", "CONT_EMAIL_TEMPLATE"), null, false);
                                    GenericValue productStoreEmailSetting = EntityUtil.getFirst(productStoreEmailSettings);
                                    if (productStoreEmailSetting != null) {
                                        // send e-mail using screen template
                                        sendMailParams.put("bodyScreenUri", productStoreEmailSetting.getString("bodyScreenLocation"));
                                        sendMailParams.put("bodyParameters", bodyParameters);
                                        sendMailParams.remove("body");
                                        tmpResult = dispatcher.runSync("sendMailFromScreen", sendMailParams, 360, true);
                                    }
                                }
                            }
                        }

                        // If the e-mail does not be sent then send normal e-mail
                        if (UtilValidate.isEmpty(tmpResult)) {
                            sendMailParams.put("body", communicationEvent.getString("content"));
                            tmpResult = dispatcher.runSync("sendMail", sendMailParams, 360, true);
                        }

                        if (tmpResult == null || ServiceUtil.isError(tmpResult)) {
                            if (ServiceUtil.getErrorMessage(tmpResult).startsWith("[ADDRERR]")) {
                                // address error; mark the communication event as BOUNCED
                                contactListCommStatusRecord.set("statusId", "COM_BOUNCED");
                                try {
                                    contactListCommStatusRecord.store();
                                } catch (GenericEntityException e) {
                                    Debug.logError(e, module);
                                    errorMessages.add(e.getMessage());
                                }
                                // deactivate from the contact list
                                try {
                                    GenericValue contactListParty = contactListPartyAndContactMech.getRelatedOne("ContactListParty", false);
                                    if (contactListParty != null) {
                                        contactListParty.set("statusId", "CLPT_INVALID");
                                        contactListParty.store();
                                    }
                                } catch (GenericEntityException e) {
                                    Debug.logError(e, module);
                                    errorMessages.add(e.getMessage());
                                }
                                continue;
                            }
                            // If the send attempt fails, just log and skip the email address
                            Debug.logError(errorCallingSendMailService + ": " + ServiceUtil.getErrorMessage(tmpResult),
                                    module);
                            errorMessages.add(errorCallingSendMailService + ": " + ServiceUtil.getErrorMessage(
                                    tmpResult));
                            continue;
                        }
                        // attach the parent communication event to the new event created when sending
                        // the mail
                        String thisCommEventId = (String) tmpResult.get("communicationEventId");
                        GenericValue thisCommEvent = EntityQuery.use(delegator).from("CommunicationEvent").where(
                                "communicationEventId", thisCommEventId).queryOne();
                        if (thisCommEvent != null) {
                            thisCommEvent.set("contactListId", contactListId);
                            thisCommEvent.set("parentCommEventId", communicationEventId);
                            thisCommEvent.store();
                        }
                        String messageId = (String) tmpResult.get("messageId");
                        contactListCommStatusRecord.set("messageId", messageId);

                        if ("Y".equals(contactList.get("singleUse"))) {

                            // Expire the ContactListParty if the list is single use and sendEmail finishes successfully
                            tmpResult = dispatcher.runSync("updateContactListParty", UtilMisc.toMap("contactListId", lastContactListPartyACM.get("contactListId"),
                                    "partyId", partyId, "fromDate", lastContactListPartyACM.get("fromDate"),
                                    "thruDate", UtilDateTime.nowTimestamp(), "userLogin", userLogin));
                            if (ServiceUtil.isError(tmpResult)) {

                                // If the expiry fails, just log and skip the email address
                                Debug.logError(errorCallingUpdateContactListPartyService + ": " + ServiceUtil.getErrorMessage(tmpResult), module);
                                errorMessages.add(errorCallingUpdateContactListPartyService + ": " + ServiceUtil.getErrorMessage(tmpResult));
                                continue;
                            }
                        }

                        // All is successful, so update the ContactListCommStatus record
                        contactListCommStatusRecord.set("statusId", "COM_COMPLETE");
                        delegator.store(contactListCommStatusRecord);

                        // Don't return a service error just because of failure for one address - just log the error and continue
                    } catch (GenericEntityException nonFatalGEE) {
                        Debug.logError(nonFatalGEE, errorInSendEmailToContactListService, module);
                        errorMessages.add(errorInSendEmailToContactListService + ": " + nonFatalGEE.getMessage());
                    } catch (GenericServiceException nonFatalGSE) {
                        Debug.logError(nonFatalGSE, errorInSendEmailToContactListService, module);
                        errorMessages.add(errorInSendEmailToContactListService + ": " + nonFatalGSE.getMessage());
                    }
                }
            } catch (GenericEntityException fatalGEE) {
                return ServiceUtil.returnError(fatalGEE.getMessage());
            }

        } catch (GenericEntityException fatalGEE) {
            return ServiceUtil.returnError(fatalGEE.getMessage());
        }

        return errorMessages.size() == 0 ? ServiceUtil.returnSuccess() : ServiceUtil.returnError(errorMessages);
    }

    public static Map<String, Object> setCommEventComplete(DispatchContext dctx, Map<String, ? extends Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        String communicationEventId = (String) context.get("communicationEventId");
        String partyIdFrom = (String) context.get("partyIdFrom");

        try {
            Map<String, Object> result = dispatcher.runSync("updateCommunicationEvent", UtilMisc.<String, Object>toMap("communicationEventId", communicationEventId,
                    "partyIdFrom", partyIdFrom, "statusId", "COM_COMPLETE", "userLogin", userLogin));
            if (ServiceUtil.isError(result)) {
                return ServiceUtil.returnError(ServiceUtil.getErrorMessage(result));
            }
        } catch (GenericServiceException esx) {
            return ServiceUtil.returnError(esx.getMessage());
        }

        return ServiceUtil.returnSuccess();
    }

    /*
     * Store an outgoing email as a communication event;
     * runs as a pre-invoke ECA on sendMail and sendMultipartMail services
     * - service should run as the 'system' user
     */
    public static Map<String, Object> createCommEventFromEmail(DispatchContext dctx, Map<String, ? extends Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Delegator delegator = dctx.getDelegator();

        GenericValue userLogin = (GenericValue) context.get("userLogin");
        String subject = (String) context.get("subject");
        String sendFrom = (String) context.get("sendFrom");
        String sendTo = (String) context.get("sendTo");
        String partyId = (String) context.get("partyId");
        String contentType = (String) context.get("contentType");
        String statusId = (String) context.get("statusId");
        String orderId = (String) context.get("orderId");
        if (statusId == null) {
            statusId = "COM_PENDING";
        }

        // get the from contact mech info
        String contactMechIdFrom = null;
        String partyIdFrom = null;
        GenericValue fromCm;
        try {
            fromCm = EntityQuery.use(delegator).from("PartyAndContactMech").where("infoString", sendFrom).orderBy("-fromDate").filterByDate().queryFirst();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        if (fromCm != null) {
            contactMechIdFrom = fromCm.getString("contactMechId");
            partyIdFrom = fromCm.getString("partyId");
        }

        // get the to contact mech info
        String contactMechIdTo = null;
        GenericValue toCm;
        try {
            toCm = EntityQuery.use(delegator).from("PartyAndContactMech").where("infoString", sendTo, "partyId", partyId).orderBy("-fromDate").filterByDate().queryFirst();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        if (toCm != null) {
            contactMechIdTo = toCm.getString("contactMechId");
        }

        Timestamp now = UtilDateTime.nowTimestamp();

        Map<String, Object> commEventMap = new HashMap<>();
        commEventMap.put("communicationEventTypeId", "EMAIL_COMMUNICATION");
        commEventMap.put("contactMechTypeId", "EMAIL_ADDRESS");
        commEventMap.put("contactMechIdFrom", contactMechIdFrom);
        commEventMap.put("contactMechIdTo", contactMechIdTo);
        commEventMap.put("statusId", statusId);

        commEventMap.put("partyIdFrom", partyIdFrom);
        commEventMap.put("partyIdTo", partyId);
        commEventMap.put("datetimeStarted", now);
        commEventMap.put("entryDate", now);

        commEventMap.put("subject", subject);
        commEventMap.put("userLogin", userLogin);
        commEventMap.put("contentMimeTypeId", contentType);
        if (UtilValidate.isNotEmpty(orderId)) {
            commEventMap.put("orderId", orderId);
        }

        Map<String, Object> createResult;
        try {
            createResult = dispatcher.runSync("createCommunicationEvent", commEventMap);
        } catch (GenericServiceException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        if (ServiceUtil.isError(createResult)) {
            return ServiceUtil.returnError(ServiceUtil.getErrorMessage(createResult));
        }
        String communicationEventId = (String) createResult.get("communicationEventId");

        Map<String, Object> result = ServiceUtil.returnSuccess();
        result.put("communicationEventId", communicationEventId);
        return result;
    }

    /*
     * Update the communication event with information from the email;
     * runs as a post-commit ECA on sendMail and sendMultiPartMail services
     * - service should run as the 'system' user
     */
    public static Map<String, Object> updateCommEventAfterEmail(DispatchContext dctx, Map<String, ? extends Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();

        GenericValue userLogin = (GenericValue) context.get("userLogin");
        String communicationEventId = (String) context.get("communicationEventId");
        MimeMessageWrapper wrapper = (MimeMessageWrapper) context.get("messageWrapper");

        Map<String, Object> commEventMap = new HashMap<>();
        commEventMap.put("communicationEventId", communicationEventId);
        commEventMap.put("subject", wrapper.getSubject());
        commEventMap.put("statusId", "COM_COMPLETE");
        commEventMap.put("datetimeEnded", UtilDateTime.nowTimestamp());
        commEventMap.put("entryDate", wrapper.getSentDate());
        commEventMap.put("messageId", wrapper.getMessageId());
        commEventMap.put("userLogin", userLogin);
        commEventMap.put("content", wrapper.getMessageBody());

        // populate the address (to/from/cc/bcc) data
        populateAddressesFromMessage(wrapper, commEventMap);

        // save the communication event
        try {
            dispatcher.runSync("updateCommunicationEvent", commEventMap);
        } catch (GenericServiceException e) {
            return ServiceUtil.returnError(e.getMessage());
        }

        // attachments
        try {
            createAttachmentContent(dispatcher, dctx.getDelegator(), wrapper, communicationEventId, userLogin);
        } catch (GenericServiceException | GenericEntityException e) {
            return ServiceUtil.returnError(e.getMessage());
        }

        return ServiceUtil.returnSuccess();
    }

    /**
     * This service is the main one for processing incoming emails.
     *
     * Its only argument is a wrapper for the JavaMail MimeMessage object.
     * From this object, all the fields, headers and content of the message can be accessed.
     *
     * The first thing this service does is try to discover the partyId of the message sender
     * by doing a reverse find on the email address. It uses the findPartyFromEmailAddress service to do this.
     *
     * It then creates a CommunicationEvent entity by calling the createCommunicationEvent service using the appropriate fields from the email and the
     * discovered partyId, if it exists, as the partyIdFrom. Note that it sets the communicationEventTypeId
     * field to AUTO_EMAIL_COMM. This is useful for tracking email generated communications.
     *
     * The service tries to find appropriate content for inclusion in the CommunicationEvent.content field.
     * If the contentType of the content starts with "text", the getContent() call returns a string and it is used.
     * If the contentType starts with "multipart", then the "parts" of the content are iterated thru and the first
     * one of mime type, "text/..." is used.
     *
     * If the contentType has a value of "multipart" then the parts of the content (except the one used in the main
     * CommunicationEvent.content field) are cycled thru and attached to the CommunicationEvent entity using the
     * createCommContentDataResource service. This happens in the EmailWorker.addAttachmentsToCommEvent method.
     *
     * However multiparts can contain multiparts. A recursive function has been added.
     *
     * -Al Byers - Hans Bakker
     * @param dctx the dispatch context
     * @param context the context
     * @return returns the result of the service execution
     */
    public static Map<String, Object> storeIncomingEmail(DispatchContext dctx, Map<String, ? extends Object> context) {
        Delegator delegator = dctx.getDelegator();
        LocalDispatcher dispatcher = dctx.getDispatcher();
        MimeMessageWrapper wrapper = (MimeMessageWrapper) context.get("messageWrapper");
        Timestamp nowTimestamp = UtilDateTime.nowTimestamp();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        Locale locale = (Locale) context.get("locale");
        String partyIdTo = null;
        String partyIdFrom = null;
        String communicationEventId = null;
        String contactMechIdFrom = null;
        String contactMechIdTo = null;

        Map<String, Object> result = null;
        try {
            Address[] addressesFrom = wrapper.getFrom();
            Address[] addressesTo = wrapper.getTo();
            Address[] addressesCC = wrapper.getCc();
            Address[] addressesBCC = wrapper.getBcc();
            String messageId = wrapper.getMessageId().replaceAll("[<>]", "");

            String aboutThisEmail = "message [" + messageId + "] from [" +
                    ((addressesFrom == null || addressesFrom[0] == null) ? "not found" : addressesFrom[0].toString()) + "] to [" +
                    ((addressesTo == null || addressesTo[0] == null) ? "not found" : addressesTo[0].toString()) + "]";

            if (Debug.verboseOn()) {
                Debug.logVerbose("Processing Incoming Email " + aboutThisEmail, module);
            }

            // ignore the message when the spam status = yes
            String spamHeaderName = EntityUtilProperties.getPropertyValue("general", "mail.spam.name", "N", delegator);
            String configHeaderValue = EntityUtilProperties.getPropertyValue("general", "mail.spam.value", delegator);
            //          only execute when config file has been set && header variable found
            if (!"N".equals(spamHeaderName) && wrapper.getHeader(spamHeaderName) != null && wrapper.getHeader(spamHeaderName).length > 0) {
                String msgHeaderValue = wrapper.getHeader(spamHeaderName)[0];
                if (msgHeaderValue != null && msgHeaderValue.startsWith(configHeaderValue)) {
                    Debug.logInfo("Incoming Email message ignored, was detected by external spam checker", module);
                    return ServiceUtil.returnSuccess(UtilProperties.getMessage(resource,
                            "PartyCommEventMessageIgnoredDetectedByExternalSpamChecker", locale));
                }
            }

            // if no 'from' addresses specified ignore the message
            if (addressesFrom == null) {
                Debug.logInfo("Incoming Email message ignored, had not 'from' email address", module);
                return ServiceUtil.returnSuccess(UtilProperties.getMessage(resource,
                        "PartyCommEventMessageIgnoredNoFromAddressSpecified", locale));
            }

            // make sure this isn't a duplicate
            List<GenericValue> commEvents;
            try {
                commEvents = EntityQuery.use(delegator).from("CommunicationEvent").where("messageId", messageId).queryList();
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
            if (!commEvents.isEmpty()) {
                Debug.logInfo("Ignoring Duplicate Email: " + aboutThisEmail, module);
                return ServiceUtil.returnSuccess(UtilProperties.getMessage(resource,
                        "PartyCommEventMessageIgnoredDuplicateMessageId", locale));
            }

            // get the related partId's
            List<Map<String, Object>> toParties = buildListOfPartyInfoFromEmailAddresses(addressesTo, userLogin, dispatcher);
            List<Map<String, Object>> ccParties = buildListOfPartyInfoFromEmailAddresses(addressesCC, userLogin, dispatcher);
            List<Map<String, Object>> bccParties = buildListOfPartyInfoFromEmailAddresses(addressesBCC, userLogin, dispatcher);

            //Get the first address from the list - this is the partyIdTo field of the CommunicationEvent
            if (!toParties.isEmpty()) {
                Map<String, Object> firstAddressTo = toParties.get(0);
                partyIdTo = (String)firstAddressTo.get("partyId");
                contactMechIdTo = (String)firstAddressTo.get("contactMechId");
            }

            String deliveredTo = wrapper.getFirstHeader("Delivered-To");
            if (deliveredTo != null) {
                // check if started with the domain name if yes remove including the dash.
                String dn = deliveredTo.substring(deliveredTo.indexOf('@') + 1, deliveredTo.length());
                if (deliveredTo.startsWith(dn)) {
                    deliveredTo = deliveredTo.substring(dn.length()+1, deliveredTo.length());
                }
            }

            // if partyIdTo not found try to find the "to" address using the delivered-to header
            if ((partyIdTo == null) && (deliveredTo != null)) {
                result = dispatcher.runSync("findPartyFromEmailAddress", UtilMisc.<String, Object>toMap("address", deliveredTo, "userLogin", userLogin));
                partyIdTo = (String)result.get("partyId");
                contactMechIdTo = (String)result.get("contactMechId");
            }
            if (userLogin.get("partyId") == null && partyIdTo != null) {
                int ch = 0;
                for (ch=partyIdTo.length(); ch > 0 && Character.isDigit(partyIdTo.charAt(ch-1)); ch--) {}
                userLogin.put("partyId", partyIdTo.substring(0,ch)); //allow services to be called to have prefix
            }

            // get the 'from' partyId
            result = getParyInfoFromEmailAddress(addressesFrom, userLogin, dispatcher);
            partyIdFrom = (String)result.get("partyId");
            contactMechIdFrom = (String)result.get("contactMechId");

            Map<String, Object> commEventMap = new HashMap<>();
            commEventMap.put("communicationEventTypeId", "AUTO_EMAIL_COMM");
            commEventMap.put("contactMechTypeId", "EMAIL_ADDRESS");
            commEventMap.put("messageId", messageId);

            String subject = wrapper.getSubject();
            commEventMap.put("subject", subject);

            // Set sent and received dates
            commEventMap.put("entryDate", nowTimestamp);
            commEventMap.put("datetimeStarted", UtilDateTime.toTimestamp(wrapper.getSentDate()));
            commEventMap.put("datetimeEnded", UtilDateTime.toTimestamp(wrapper.getReceivedDate()));

            // default role types (_NA_)
            commEventMap.put("roleTypeIdFrom", "_NA_");
            commEventMap.put("roleTypeIdTo", "_NA_");

            // get the content(type) part
            String messageBodyContentType = wrapper.getMessageBodyContentType();
            if (messageBodyContentType.indexOf(';') > -1) {
                messageBodyContentType = messageBodyContentType.substring(0, messageBodyContentType.indexOf(';'));
            }

            // select the plain text bodypart
            String messageBody = null;
            if (wrapper.getMainPartCount() > 1) {
                for (int ind=0; ind < wrapper.getMainPartCount(); ind++) {
                    BodyPart p = wrapper.getPart(ind + "");
                    if (p.getContentType().toLowerCase(Locale.getDefault()).indexOf("text/plain") > -1) {
                        messageBody = (String) p.getContent();
                        break;
                    }
                }
            }

            if (messageBody == null) {
                messageBody = wrapper.getMessageBody();
            }

            commEventMap.put("content", messageBody);
            commEventMap.put("contentMimeTypeId", messageBodyContentType.toLowerCase(Locale.getDefault()));

            // check for for a reply to communication event (using in-reply-to the parent messageID)
            String[] inReplyTo = wrapper.getHeader("In-Reply-To");
            if (inReplyTo != null && inReplyTo[0] != null) {
                GenericValue parentCommEvent = null;
                try {
                    parentCommEvent = EntityQuery.use(delegator).from("CommunicationEvent").where("messageId", inReplyTo[0].replaceAll("[<>]", "")).queryFirst();
                } catch (GenericEntityException e) {
                    Debug.logError(e, module);
                }
                if (parentCommEvent != null) {
                    String parentCommEventId = parentCommEvent.getString("communicationEventId");
                    String orgCommEventId = parentCommEvent.getString("origCommEventId");
                    if (orgCommEventId == null) {
                        orgCommEventId = parentCommEventId;
                    }
                    commEventMap.put("parentCommEventId", parentCommEventId);
                    commEventMap.put("origCommEventId", orgCommEventId);
                }
            }

            // populate the address (to/from/cc/bcc) data
            populateAddressesFromMessage(wrapper, commEventMap);

            // store from/to parties, but when not found make a note of the email to/from address in the workEffort Note Section.
            String commNote = "";
            if (partyIdFrom != null) {
                commEventMap.put("partyIdFrom", partyIdFrom);
                commEventMap.put("contactMechIdFrom", contactMechIdFrom);
            } else {
                commNote += "Sent from: " +  ((InternetAddress)addressesFrom[0]).getAddress() + "; ";
                commNote += "Sent Name from: " + ((InternetAddress)addressesFrom[0]).getPersonal() + "; ";
            }

            if (partyIdTo != null) {
                commEventMap.put("partyIdTo", partyIdTo);
                commEventMap.put("contactMechIdTo", contactMechIdTo);
            } else {
                commNote += "Sent to: " + ((InternetAddress)addressesTo[0]).getAddress()  + "; ";
                if (deliveredTo != null) {
                    commNote += "Delivered-To: " + deliveredTo + "; ";
                }
            }

            commNote += "Sent to: " + ((InternetAddress)addressesTo[0]).getAddress()  + "; ";
            commNote += "Delivered-To: " + deliveredTo + "; ";

            if (partyIdTo != null && partyIdFrom != null) {
                commEventMap.put("statusId", "COM_ENTERED");
            } else {
                commEventMap.put("statusId", "COM_UNKNOWN_PARTY");
            }
            if (commNote.length() > 255) {
                commNote = commNote.substring(0,255);
            }

            if (!("".equals(commNote))) {
                commEventMap.put("note", commNote);
            }

            commEventMap.put("userLogin", userLogin);

            // Populate the CommunicationEvent.headerString field with the email headers
            StringBuilder headerString = new StringBuilder();
            Enumeration<?> headerLines = wrapper.getMessage().getAllHeaderLines();
            while (headerLines.hasMoreElements()) {
                headerString.append(System.getProperty("line.separator"));
                headerString.append(headerLines.nextElement());
            }
            String header = headerString.toString();
            commEventMap.put("headerString", header.replaceAll("[<>]", ""));

            result = dispatcher.runSync("createCommunicationEvent", commEventMap);
            communicationEventId = (String)result.get("communicationEventId");
            Debug.logInfo("Persisting New Email: " + aboutThisEmail + " into CommunicationEventId: " + communicationEventId, module);

            // handle the attachments
            createAttachmentContent(dispatcher, delegator, wrapper, communicationEventId, userLogin);

            // For all addresses create a CommunicationEventRoles
            createCommEventRoles(userLogin, delegator, dispatcher, communicationEventId, toParties, "ADDRESSEE");
            createCommEventRoles(userLogin, delegator, dispatcher, communicationEventId, ccParties, "CC");
            createCommEventRoles(userLogin, delegator, dispatcher, communicationEventId, bccParties, "BCC");

            // get the related work effort info
            List<Map<String, Object>> toWorkEffortInfos = buildListOfWorkEffortInfoFromEmailAddresses(addressesTo, userLogin, dispatcher);
            List<Map<String, Object>> ccWorkEffortInfos = buildListOfWorkEffortInfoFromEmailAddresses(addressesCC, userLogin, dispatcher);
            List<Map<String, Object>> bccWorkEffortInfos = buildListOfWorkEffortInfoFromEmailAddresses(addressesBCC, userLogin, dispatcher);

            // For all WorkEffort addresses create a CommunicationEventWorkEffs
            createCommunicationEventWorkEffs(userLogin, dispatcher, toWorkEffortInfos, communicationEventId);
            createCommunicationEventWorkEffs(userLogin, dispatcher, ccWorkEffortInfos, communicationEventId);
            createCommunicationEventWorkEffs(userLogin, dispatcher, bccWorkEffortInfos, communicationEventId);

            Map<String, Object> results = ServiceUtil.returnSuccess();
            results.put("communicationEventId", communicationEventId);
            results.put("statusId", commEventMap.get("statusId"));
            return results;
        } catch (MessagingException | GenericServiceException | GenericEntityException | IOException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
    }

    private static void populateAddressesFromMessage(MimeMessageWrapper wrapper, Map<String, Object> commEventMap) {
        // Retrieve all the addresses from the email
        Address[] addressesFrom = wrapper.getFrom();
        Address[] addressesTo = wrapper.getTo();
        Address[] addressesCC = wrapper.getCc();
        Address[] addressesBCC = wrapper.getBcc();

        Set<String> emailAddressesFrom = new TreeSet<>();
        Set<String> emailAddressesTo = new TreeSet<>();
        Set<String> emailAddressesCC = new TreeSet<>();
        Set<String> emailAddressesBCC = new TreeSet<>();
        for (Address element : addressesFrom) {
            emailAddressesFrom.add(((InternetAddress) element).getAddress());
        }
        for (Address element : addressesTo) {
            emailAddressesTo.add(((InternetAddress) element).getAddress());
        }
        if (addressesCC != null) {
            for (Address element : addressesCC) {
                emailAddressesCC.add(((InternetAddress) element).getAddress());
            }
        }
        if (addressesBCC != null) {
            for (Address element : addressesBCC) {
                emailAddressesBCC.add(((InternetAddress) element).getAddress());
            }
        }
        String fromString = StringUtil.join(UtilMisc.toList(emailAddressesFrom), ",");
        String toString = StringUtil.join(UtilMisc.toList(emailAddressesTo), ",");
        String ccString = StringUtil.join(UtilMisc.toList(emailAddressesCC), ",");
        String bccString = StringUtil.join(UtilMisc.toList(emailAddressesBCC), ",");

        if (UtilValidate.isNotEmpty(fromString)) {
            commEventMap.put("fromString", fromString);
        }
        if (UtilValidate.isNotEmpty(toString)) {
            commEventMap.put("toString", toString);
        }
        if (UtilValidate.isNotEmpty(ccString)) {
            commEventMap.put("ccString", ccString);
        }
        if (UtilValidate.isNotEmpty(bccString)) {
            commEventMap.put("bccString", bccString);
        }
    }

    private static List<String> getCommEventAttachmentNames(final Delegator delegator, final String communicationEventId) throws GenericEntityException {
        List<GenericValue> commEventContentAssocList = EntityQuery.use(delegator)
            .from("CommEventContentDataResource")
            .where(EntityCondition.makeCondition("communicationEventId", communicationEventId))
            .filterByDate()
            .queryList();

        List<String> attachmentNames = new ArrayList<>();
        for (GenericValue commEventContentAssoc : commEventContentAssocList) {
            String dataResourceName = commEventContentAssoc.getString("drDataResourceName");
            attachmentNames.add(dataResourceName);
        }

        return attachmentNames;
    }

    private static void createAttachmentContent(LocalDispatcher dispatcher, Delegator delegator, MimeMessageWrapper wrapper, String communicationEventId, GenericValue userLogin) throws GenericServiceException, GenericEntityException {
        // handle the attachments
        String subject = wrapper.getSubject();
        List<String> attachmentIndexes = wrapper.getAttachmentIndexes();
        List<String> currentAttachmentNames = getCommEventAttachmentNames(delegator, communicationEventId);

        if (attachmentIndexes.size() > 0) {
            Debug.logInfo("=== message has attachments [" + attachmentIndexes.size() + "] =====", module);
            for (String attachmentIdx : attachmentIndexes) {
                String attFileName = wrapper.getPartFilename(attachmentIdx);
                if (currentAttachmentNames.contains(attFileName)) {
                    Debug.logWarning(String.format("CommunicationEvent [%s] already has attachment named '%s'", communicationEventId, attFileName), module);
                    continue;
                }

                Map<String, Object> attachmentMap = new HashMap<>();
                attachmentMap.put("communicationEventId", communicationEventId);
                attachmentMap.put("contentTypeId", "DOCUMENT");
                attachmentMap.put("mimeTypeId", "text/html");
                attachmentMap.put("userLogin", userLogin);
                if (subject != null && subject.length() > 80) {
                    subject = subject.substring(0,80); // make sure not too big for database field. (20 characters for filename)
                }

                String attContentType = wrapper.getPartContentType(attachmentIdx);
                if (attContentType != null && attContentType.indexOf(';') > -1) {
                    attContentType = attContentType.toLowerCase(Locale.getDefault()).substring(0, attContentType.indexOf(';'));
                }

                if (UtilValidate.isNotEmpty(attFileName)) {
                    attachmentMap.put("contentName", attFileName);
                    attachmentMap.put("description", subject + "-" + attachmentIdx);
                } else {
                    attachmentMap.put("contentName", subject + "-" + attachmentIdx);
                }

                attachmentMap.put("drMimeTypeId", attContentType);
                if (attContentType != null && attContentType.startsWith("text")) {
                    String text = wrapper.getPartText(attachmentIdx);
                    attachmentMap.put("drDataResourceTypeId", "ELECTRONIC_TEXT");
                    attachmentMap.put("textData", text);
                } else {
                    ByteBuffer data = wrapper.getPartByteBuffer(attachmentIdx);
                    if (Debug.infoOn()) {
                        Debug.logInfo("Binary attachment size: " + data.limit(), module);
                    }
                    attachmentMap.put("drDataResourceName", attFileName);
                    attachmentMap.put("imageData", data);
                    attachmentMap.put("drDataResourceTypeId", "IMAGE_OBJECT"); // TODO: why always use IMAGE
                    attachmentMap.put("_imageData_contentType", attContentType);
                }

                // save the content
                dispatcher.runSync("createCommContentDataResource", attachmentMap);
            }
        }
    }

    private static void createCommEventRoles(GenericValue userLogin, Delegator delegator, LocalDispatcher dispatcher, String communicationEventId, List<Map<String, Object>> parties, String roleTypeId) {
        // It's not clear what the "role" of this communication event should be, so we'll just put _NA_
        // check and see if this role was already created and ignore if true
        try {
            for (Map<String, Object> result : parties) {
                String partyId = (String) result.get("partyId");
                GenericValue commEventRole = EntityQuery.use(delegator).from("CommunicationEventRole")
                        .where("communicationEventId", communicationEventId, "partyId", partyId, "roleTypeId", roleTypeId)
                        .queryOne();
                if (commEventRole == null) {
                    Map<String, Object> input = UtilMisc.toMap("communicationEventId", communicationEventId,
                            "partyId", partyId, "roleTypeId", roleTypeId, "userLogin", userLogin,
                            "contactMechId", (String) result.get("contactMechId"),
                            "statusId", "COM_ROLE_CREATED");
                    dispatcher.runSync("createCommunicationEventRole", input);
                }
            }
        } catch (GenericServiceException | GenericEntityException e) {
            Debug.logError(e, module);
        }
    }

    private static void createCommunicationEventWorkEffs(GenericValue userLogin, LocalDispatcher dispatcher, List<Map<String, Object>> workEffortInfos, String communicationEventId) {
        // create relationship between communication event and work efforts
        try {
            for (Map<String, Object> result : workEffortInfos) {
                String workEffortId = (String) result.get("workEffortId");
                dispatcher.runSync("createCommunicationEventWorkEff", UtilMisc.toMap("workEffortId", workEffortId, "communicationEventId", communicationEventId, "userLogin", userLogin));
            }
        } catch (GenericServiceException e) {
            Debug.logError(e, module);
        }
    }

    /*
     * Helper method to retrieve the party information from the first email address of the Address[] specified.
     */
    private static Map<String, Object> getParyInfoFromEmailAddress(Address [] addresses, GenericValue userLogin, LocalDispatcher dispatcher) throws GenericServiceException {
        InternetAddress emailAddress = null;
        Map<String, Object> map = null;
        Map<String, Object> result = null;

        if (addresses == null) {
            return null;
        }

        if (addresses.length > 0) {
            Address addr = addresses[0];
            if (addr instanceof InternetAddress) {
                emailAddress = (InternetAddress)addr;
            }
        }

        if (emailAddress != null) {
            map = new HashMap<>();
            map.put("address", emailAddress.getAddress());
            map.put("userLogin", userLogin);
            result = dispatcher.runSync("findPartyFromEmailAddress", map);
        }

        return result;
    }

    /*
     * Calls findPartyFromEmailAddress service and returns a List of the results for the array of addresses
     */
    private static List<Map<String, Object>> buildListOfPartyInfoFromEmailAddresses(Address [] addresses, GenericValue userLogin, LocalDispatcher dispatcher) throws GenericServiceException {
        InternetAddress emailAddress = null;
        Map<String, Object> result = null;
        List<Map<String, Object>> tempResults = new LinkedList<>();

        if (addresses != null) {
            for (Address addr: addresses) {
                if (addr instanceof InternetAddress) {
                    emailAddress = (InternetAddress)addr;

                    result = dispatcher.runSync("findPartyFromEmailAddress",
                            UtilMisc.toMap("address", emailAddress.getAddress(), "userLogin", userLogin));
                    if (result.get("partyId") != null) {
                        tempResults.add(result);
                    }
                }
            }
        }
        return tempResults;
    }

    /*
     * Gets WorkEffort info from e-mail address and returns a List of the results for the array of addresses
     */
    private static List<Map<String, Object>> buildListOfWorkEffortInfoFromEmailAddresses(Address [] addresses, GenericValue userLogin, LocalDispatcher dispatcher) throws GenericServiceException {
        InternetAddress emailAddress = null;
        Map<String, Object> result = null;
        Delegator delegator = dispatcher.getDelegator();
        List<Map<String, Object>> tempResults = new LinkedList<>();
        String caseInsensitiveEmail = EntityUtilProperties.getPropertyValue("general", "mail.address.caseInsensitive", "N", delegator);

        if (addresses != null) {
            for (Address addr: addresses) {
                if (addr instanceof InternetAddress) {
                    emailAddress = (InternetAddress)addr;
                    Map<String, String> inputFields = new HashMap<>();
                    inputFields.put("infoString", emailAddress.getAddress());
                    inputFields.put("infoString_ic", caseInsensitiveEmail);
                    result = dispatcher.runSync("performFind", UtilMisc.<String, Object>toMap("entityName",
                            "WorkEffortContactMechView", "inputFields", inputFields, "userLogin", userLogin));
                    try (EntityListIterator listIt = (EntityListIterator) result.get("listIt")) {
                        List<GenericValue> list = listIt.getCompleteList();
                        List<GenericValue> filteredList = EntityUtil.filterByDate(list);
                        tempResults.addAll(filteredList);
                    } catch (GenericEntityException e) {
                        Debug.logError(e, module);
                    }
                }
            }
        }
        return tempResults;
    }

    /*
     * Service to process incoming email and look for a bounce message. If the email is indeed a bounce message
     * the CommunicationEvent will be updated with the proper COM_BOUNCED status.
     */
    public static Map<String,Object> processBouncedMessage(DispatchContext dctx, Map<String, ? extends Object> context) {
        Debug.logInfo("Running process bounced message check...", module);
        MimeMessageWrapper wrapper = (MimeMessageWrapper) context.get("messageWrapper");

        LocalDispatcher dispatcher = dctx.getDispatcher();
        Delegator delegator = dctx.getDelegator();

        int parts = wrapper.getMainPartCount();

        if (parts >= 3) { // it must have all three parts in order to process correctly
            // get the second part (delivery report)
            String contentType = wrapper.getPartContentType("1"); // index 1 should be the second part
            if (contentType != null && "message/delivery-status".equalsIgnoreCase(contentType)) {
                Debug.logInfo("Delivery status report part found; processing...", module);

                // get the content of the part
                String part2Text = wrapper.getPartRawText("1");
                if (part2Text == null) {
                    part2Text = "";
                }
                if (Debug.verboseOn()) {
                     Debug.logVerbose("Part 2 Text :\n\n" + part2Text, module);
                }

                // find the "Action" element and obtain its value (looking for "failed")
                Pattern p2 = Pattern.compile("^Action: (.*)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
                Matcher m2 = p2.matcher(part2Text);
                String action = null;
                if (m2.find()) {
                    action = m2.group(1);
                }

                if (action != null && "failed".equalsIgnoreCase(action)) {
                    // message bounced -- get the original message
                    String part3Text = wrapper.getPartRawText("2"); // index 2 should be the third part
                    if (part3Text == null) {
                        part3Text = "";
                    }
                    if (Debug.verboseOn()) {
                         Debug.logVerbose("Part 3 Text :\n\n" + part3Text, module);
                    }

                    // find the "Message-Id" element and obtain its value (looking for "failed")
                    Pattern p3 = Pattern.compile("^Message-Id: (.*)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
                    Matcher m3 = p3.matcher(part3Text);
                    String messageId = null;
                    if (m3.find()) {
                        Debug.logInfo("Found message-id : " + m3.group(), module);
                        messageId = m3.group(1);
                    }

                    // find the matching communication event
                    if (messageId != null) {
                        List<GenericValue> values;
                        try {
                            values = EntityQuery.use(delegator).from("CommunicationEvent").where("messageId", messageId).queryList();
                        } catch (GenericEntityException e) {
                            Debug.logError(e, module);
                            return ServiceUtil.returnError(e.getMessage());
                        }
                        if (UtilValidate.isNotEmpty(values)) {
                            // there should be only one; unique key
                            GenericValue value = values.get(0);

                            // update the communication event status
                            Map<String, Object> updateCtx = new HashMap<>();
                            updateCtx.put("communicationEventId", value.getString("communicationEventId"));
                            updateCtx.put("statusId", "COM_BOUNCED");
                            updateCtx.put("userLogin", context.get("userLogin"));
                            Map<String, Object> result;
                            try {
                                result = dispatcher.runSync("updateCommunicationEvent", updateCtx);
                            } catch (GenericServiceException e) {
                                Debug.logError(e, module);
                                return ServiceUtil.returnError(e.getMessage());
                            }
                            if (ServiceUtil.isError(result)) {
                                return ServiceUtil.returnError(ServiceUtil.getErrorMessage(result));
                            }
                        } else {
                            if (Debug.infoOn()) {
                                Debug.logInfo("Unable to find CommunicationEvent with the matching messageId : " + messageId, module);
                            }

                            // no communication events found for that message ID; possible this is a NEWSLETTER
                            try {
                                values = EntityQuery.use(delegator).from("ContactListCommStatus").where("messageId", messageId).queryList();
                            } catch (GenericEntityException e) {
                                Debug.logError(e, module);
                                return ServiceUtil.returnError(e.getMessage());
                            }
                            if (UtilValidate.isNotEmpty(values)) {
                                // there should be only one; unique key
                                GenericValue value = values.get(0);

                                Map<String, Object> updateCtx = new HashMap<>();
                                updateCtx.put("communicationEventId", value.getString("communicationEventId"));
                                updateCtx.put("contactListId", value.getString("contactListId"));
                                updateCtx.put("contactMechId", value.getString("contactMechId"));
                                updateCtx.put("partyId", value.getString("partyId"));
                                updateCtx.put("statusId", "COM_BOUNCED");
                                updateCtx.put("userLogin", context.get("userLogin"));
                                Map<String, Object> result;
                                try {
                                    result = dispatcher.runSync("updateContactListCommStatus", updateCtx);
                                } catch (GenericServiceException e) {
                                    Debug.logError(e, module);
                                    return ServiceUtil.returnError(e.getMessage());
                                }
                                if (ServiceUtil.isError(result)) {
                                    return ServiceUtil.returnError(ServiceUtil.getErrorMessage(result));
                                }
                            } else {
                                if (Debug.infoOn()) {
                                    Debug.logInfo("Unable to find ContactListCommStatus with the matching messageId : "  + messageId, module);
                                }
                            }
                        }
                    } else {
                        Debug.logWarning("No message ID attached to part", module);
                    }
                }
            }
        }

        return ServiceUtil.returnSuccess();
    }

    public static Map<String,Object> logIncomingMessage(DispatchContext dctx, Map<String, ? extends Object> context) {
        MimeMessageWrapper wrapper = (MimeMessageWrapper) context.get("messageWrapper");
        Debug.logInfo("Message recevied         : " + wrapper.getSubject(), module);
        Debug.logInfo("-- Content Type          : " + wrapper.getContentType(), module);
        Debug.logInfo("-- Number of parts       : " + wrapper.getMainPartCount(), module);
        Debug.logInfo("-- Number of attachments : " + wrapper.getAttachmentIndexes().size(), module);
        Debug.logInfo("-- Message ID            : " + wrapper.getMessageId(), module);

        Debug.logInfo("### MESSAGE ###\n\n" + wrapper.getMessageBody(), module);

        List<String> attachmentIndexes = wrapper.getAttachmentIndexes();
        if (attachmentIndexes.size() > 0) {
            Debug.logInfo("### ATTACHMENTS ###", module);
            for (String idx : attachmentIndexes) {
                Debug.logInfo("### -- Filename          : " + wrapper.getPartFilename(idx), module);
                Debug.logInfo("### -- Content Type      : " + wrapper.getPartContentType(idx), module);
            }
        }

        return ServiceUtil.returnSuccess();

    }

    /*
     * Event which marks a communication event as read, and returns a 1px image to the browser/mail client
     * Is updated because the read status is now stored in the communicationEventRole
     * This services is updated but could not be tested. assumed is "read" for partyIdTo on the commevent
     */
    public static String markCommunicationAsRead(HttpServletRequest request, HttpServletResponse response) {
        String communicationEventId = null;

        // pull the communication event from path info, so we can hide the process from the user
        String pathInfo = request.getPathInfo();
        String[] pathParsed = pathInfo.split("/", 3);
        if (pathParsed.length > 2) {
            pathInfo = pathParsed[2];
        } else {
            pathInfo = null;
        }
        if (pathInfo != null && pathInfo.indexOf('/') > -1) {
            pathParsed = pathInfo.split("/");
            communicationEventId = pathParsed[0];
        }

        // update the communication event
        if (communicationEventId != null) {
            Debug.logInfo("Marking communicationEventId [" + communicationEventId + "] from path info : " + request.getPathInfo() + " as read.", module);
            Delegator delegator = (Delegator) request.getAttribute("delegator");
            GenericValue communicationEvent = null;
            try {
                communicationEvent = EntityQuery.use(delegator).from("CommunicationEvent").where("communicationEventId", communicationEventId).cache().queryOne();
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            LocalDispatcher dispatcher = (LocalDispatcher) request.getAttribute("dispatcher");
            try {
                dispatcher.runAsync("setCommEventRoleToRead", UtilMisc.toMap("communicationEventId", communicationEventId, "partyId", communicationEvent.getString("partyIdTo")));
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
            }
        }

        // return the 1px image (spacer.gif)
        URL imageUrl;
        try {
            imageUrl = FlexibleLocation.resolveLocation("component://common-theme/webapp/images/spacer.gif");
            try (InputStream imageStream = imageUrl.openStream()) {
            UtilHttp.streamContentToBrowser(response, imageStream, 43, "image/gif", null);
            }
        } catch (MalformedURLException e) {
            Debug.logError(e, module);
        } catch (IOException e) {
            Debug.logError(e, module);
        }

        // return null to not return any view
        return null;
    }
}
