<?php
/**
 *
 * SugarCRM Community Edition is a customer relationship management program developed by
 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
 *
 * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.
 * Copyright (C) 2011 - 2018 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * 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 Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along with
 * this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "Powered by
 * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM".
 */


if (!defined('sugarEntry') || !sugarEntry) {
    die('Not A Valid Entry Point');
}

require_once("include/JSON.php");

#[\AllowDynamicProperties]
class SugarEmailAddress extends SugarBean
{
    const ERR_INVALID_REQUEST_NO_USER_PROFILE_PAGE_SAVE_ACTION = 1;
    const ERR_INVALID_REQUEST_NO_REQUEST = 2;
    const ERR_INVALID_REQUEST_NO_EMAIL_INFOS = 3;
    const ERR_INVALID_REQUEST_NO_VALID_EMAIL_ADDR_IN_REQUEST = 4;
    const ERR_INVALID_REQUEST_VALID_USER_IS_SET_BUT_NO_IN_REQUEST = 5;
    const ERR_PRIMARY_EMAIL_IS_NOT_SELECTED = 6;
    const ERR_REPLYTO_EMAIL_IS_NOT_SELECTED = 7;
    const ERR_INVALID_REQUEST_NO_VALID_USER_IN_REQUEST = 8;
    const ERR_INVALID_REQUEST_MORE_THAN_ONE_USER_IN_REQUEST = 9;
    const ERR_SOME_EMAILS_WERE_NOT_SAVED_OR_UPDATED = 10;

    // Opt In Flags (for Ticks)
    const COI_FLAG_OPT_IN = 'OPT_IN';
    const COI_FLAG_OPT_IN_DISABLED = 'OPT_IN_DISABLED';
    const COI_FLAG_OPT_IN_PENDING_EMAIL_CONFIRMED = 'OPT_IN_PENDING_EMAIL_CONFIRMED';
    const COI_FLAG_OPT_IN_PENDING_EMAIL_FAILED = 'OPT_IN_PENDING_EMAIL_FAILED';
    const COI_FLAG_OPT_IN_PENDING_EMAIL_NOT_SENT = 'OPT_IN_PENDING_EMAIL_NOT_SENT';
    const COI_FLAG_OPT_IN_PENDING_EMAIL_SENT = 'OPT_IN_PENDING_EMAIL_SENT';
    const COI_FLAG_OPT_OUT = 'OPT_OUT';
    const COI_FLAG_UNKNOWN_OPT_IN_STATUS = 'UNKNOWN_OPT_IN_STATUS';
    const COI_FLAG_INVALID = 'INVALID';
    const COI_FLAG_NO_OPT_IN_STATUS = 'NO_OPT_IN_STATUS';

    // Opt In Status
    const COI_STAT_DISABLED = 'not-opt-in';
    const COI_STAT_OPT_IN = 'opt-in';
    const COI_STAT_CONFIRMED_OPT_IN = 'confirmed-opt-in';

    /** @var boolean $tracker_visibility */
    public $tracker_visibility = false;

    /**
     * @var string $table_name
     */
    public $table_name = 'email_addresses';

    /**
     * @var string $module_name
     */
    public $module_name = "EmailAddresses";

    /** @var string $module_dir */
    public $module_dir = 'EmailAddresses';

    /** @var string  $object_name */
    public $object_name = 'EmailAddress';


    /**
     * bug 40068, According to rules in page 6 of http://www.apps.ietf.org/rfc/rfc3696.html#sec-3,
     * allowed special characters ! # $ % & ' * + - / = ?  ^ _ ` . { | } ~ in local part
     * @var string  $regex
     */
    public $regex = "/^(?:['\.\-\+&#!\$\*=\?\^_`\{\}~\/\w]+)@(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|\w+(?:[\.-]*\w+)*(?:\.[\w-]{2,})+)\$/";

    /** @var bool $disable_custom_fields */
    public $disable_custom_fields = true;

    /**
     * @var DBManager
     */
    public $db;
    /**
     * @var Sugar_Smarty  $smarty
     */
    public $smarty;

    /** @var EmailAddress[] $addresses email addresses*/
    public $addresses = array();

    /**
     * @var string $view
     */
    public $view = '';

    /**
     * @var
     */
    private $stateBeforeWorkflow;

    /**
     * @var string $email_address
     */
    public $email_address;

    /**
     * @var string $email_address_caps
     */
    public $email_address_caps;

    public static $count = 0;

    /**
     * @var int
     */
    public $index;


    /**
     * @see SugarEmailAddress::COI_STAT_DISABLED
     * @see SugarEmailAddress::COI_STAT_OPT_IN
     * @see SugarEmailAddress::COI_STAT_CONFIRMED_OPT_IN
     * @var string $confirm_opt_in
     */
    public $confirm_opt_in = '';

    /**
     * @var int|bool $opt_out
     */
    public $opt_out;

    /**
     * @var int|bool $invalid_email
     */
    public $invalid_email;

    /**
     * @var TimeDate $confirm_opt_in_date
     */
    public $confirm_opt_in_date;

    /**
     * @var TimeDate $confirm_opt_in_sent_date
     */
    public $confirm_opt_in_sent_date;

    /**
     * @var TimeDate $confirm_opt_in_fail_date
     */
    public $confirm_opt_in_fail_date;

    /**
     *
     * @var string
     */
    public $confirm_opt_in_token;

    /**
     * @var array $doNotDisplayOptInTickForModule
     */
    protected static $doNotDisplayOptInTickForModule = array(
        'Users',
        'Employees'
    );
    
    /**
     * For saveAtUserProfile() method to telling what
     * went wrong at the last call.
     *
     * @var array
     */
    public $lastSaveAtUserProfileErrors = [];

    /**
     * Sole constructor
     */
    public function __construct()
    {
        parent::__construct();
        $this->index = self::$count;
        self::$count++;
    }



    /**
     * Legacy email address handling.  This is to allow support for SOAP or customizations
     * @param SugarBean $bean
     */
    public function handleLegacySave($bean)
    {
        if ($this->needsToParseLegacyAddresses($bean)) {
            $this->parseLegacyEmailAddresses($bean);
        }
        $this->populateAddresses($bean->id, $bean->module_dir, array(), '');
        if (isset($_REQUEST) && isset($_REQUEST[$bean->module_dir . '_email_widget_id'])) {
            $this->populateLegacyFields($bean);
        }
    }

    /**
     * @param SugarBean $bean
     * @return bool
     */
    private function needsToParseLegacyAddresses($bean)
    {
        if (
            !isset($_REQUEST)
            || !isset($_REQUEST[$bean->module_dir . '_email_widget_id'])
            || !isset($_REQUEST['massupdate'])
        ) {
            if (empty($this->addresses)) {
                $this->addresses = array();
                $optOut = (isset($bean->email_opt_out) && $bean->email_opt_out == '1');
                $invalid = (isset($bean->invalid_email) && $bean->invalid_email == '1');

                $isPrimary = true;
                for ($i = 1; $i <= 10; $i++) {
                    $email = 'email' . $i;
                    if (isset($bean->$email) && !empty($bean->$email)) {
                        $opt_out_field = $email . '_opt_out';
                        $invalid_field = $email . '_invalid';
                        $field_optOut = (isset($bean->$opt_out_field)) ? $bean->$opt_out_field : $optOut;
                        $field_invalid = (isset($bean->$invalid_field)) ? $bean->$invalid_field : $invalid;
                        $this->addAddress($bean->$email, $isPrimary, false, $field_invalid, $field_optOut);
                        $isPrimary = false;
                    }
                }
            }
        }
    }

    /**
     * User Profile specific save email addresses,
     * returns:
     * true - success
     * false - error
     *
     * Note:
     * This function could head to many errors but return
     * value is false in each case.
     * It is confusing because the ambiguous return value.
     * This function also stores the error code(s) in
     * array SugarEmailAddress::$lastSaveAtUserProfileErrors
     *
     * @param array $request $_REQUEST
     * @return bool
     */
    public function saveAtUserProfile($request)
    {
        $this->lastSaveAtUserProfileErrors = [];

        // validate the request first

        if (!$this->isUserProfileEditViewPageSaveAction($request)) {
            $GLOBALS['log']->error('Invalid Referrer: '.
                'expected the Save action to be called from the User\'s Profile Edit View');
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_NO_USER_PROFILE_PAGE_SAVE_ACTION;
            return false;
        }

        if (!$request) {
            $GLOBALS['log']->error('This function requires a request array');
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_NO_REQUEST;
            return false;
        }

        // first grab the needed information from a messy request

        $neededRequest = array();
        foreach ($request as $key => $value) {
            if (preg_match('/^Users\d+emailAddress/', $key)) {
                $neededRequest[$key] = $value;
            }
        }

        if (!$neededRequest) {
            $GLOBALS['log']->error('Email info is not found in request');
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_NO_EMAIL_INFOS;
            return false;
        }

        // re-parsing the request and convert into a useful format

        $usefulRequest = array();
        foreach ($neededRequest as $key => $value) {
            if (preg_match('/^Users(\d+)emailAddress(\d+)/', $key, $matches)) {
                $usefulRequest['Users'][$matches[1]]['emailAddress'][$matches[2]] = array(
                    'email' => $neededRequest["Users{$matches[1]}emailAddress{$matches[2]}"],
                    'id' => $neededRequest["Users{$matches[1]}emailAddressId{$matches[2]}"],
                    'primary' => false,
                    'replyTo' => false,
                );
            }
        }

        if (!$usefulRequest) {
            $GLOBALS['log']->error('Cannot find valid email address(es) in request');
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_NO_VALID_EMAIL_ADDR_IN_REQUEST;
            return false;
        }

        if (!isset($usefulRequest['Users']) || !$usefulRequest['Users']) {
            $GLOBALS['log']->error('Cannot find valid user in request');
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_VALID_USER_IS_SET_BUT_NO_IN_REQUEST;
            return false;
        }

        // find the selected primary and replyTo

        $primary = null;
        $replyTo = null;
        foreach ($usefulRequest['Users'] as $ukey => $user) {
            if (
                !$primary &&
                isset($neededRequest["Users{$ukey}emailAddressPrimaryFlag"]) &&
                $neededRequest["Users{$ukey}emailAddressPrimaryFlag"]
            ) {
                $primary = $neededRequest["Users{$ukey}emailAddressPrimaryFlag"];
            }

            if (
                !$replyTo &&
                isset($neededRequest["Users{$ukey}emailAddressReplyToFlag"]) &&
                $neededRequest["Users{$ukey}emailAddressReplyToFlag"]
            ) {
                $replyTo = $neededRequest["Users{$ukey}emailAddressReplyToFlag"];
            }

            // founds?
            if ($primary && $replyTo) {
                break;
            }
        }

        // add primary and replyTo into useful formatted request

        if ($primary && preg_match('/^Users(\d+)emailAddress(\d+)$/', $primary, $matches)) {
            $usefulRequest['Users'][$matches[1]]['emailAddress'][$matches[2]]['primary'] = true;
        } else {
            $GLOBALS['log']->warn("Primary email is not selected.");
            $this->lastSaveAtUserProfileErrors[] = self::ERR_PRIMARY_EMAIL_IS_NOT_SELECTED;
        }

        if ($replyTo && preg_match('/^Users(\d+)emailAddress(\d+)$/', $replyTo, $matches)) {
            $usefulRequest['Users'][$matches[1]]['emailAddress'][$matches[2]]['replyTo'] = true;
        } else {
            $GLOBALS['log']->warn("Reply-to email is not selected.");
            $this->lastSaveAtUserProfileErrors[] = self::ERR_REPLYTO_EMAIL_IS_NOT_SELECTED;
        }

        if (count($usefulRequest['Users']) < 1) {
            $GLOBALS['log']->error("Cannot find valid user in request");
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_NO_VALID_USER_IN_REQUEST;
            return false;
        }

        if (count($usefulRequest['Users']) > 1) {
            $GLOBALS['log']->warn("Expected only one user in request");
            $this->lastSaveAtUserProfileErrors[] = self::ERR_INVALID_REQUEST_MORE_THAN_ONE_USER_IN_REQUEST;
        }

        $return = true;
        foreach ($usefulRequest['Users'] as $user) {
            foreach ($user['emailAddress'] as $email) {
                if (!$this->handleEmailSaveAtUserProfile($email['id'], $email['email'], $email['primary'], $email['replyTo'])) {
                    $GLOBALS['log']->warn("Some emails were not saved or updated: {$email['id']} ({$email['email']})");
                    $return = false;
                }
            }
        }

        if ($return === false) {
            $this->lastSaveAtUserProfileErrors[] = self::ERR_SOME_EMAILS_WERE_NOT_SAVED_OR_UPDATED;
        }

        return $return;
    }


    /**
     * Handle save on User Profile specific Email Addresses,
     * returns:
     * true - success
     * false - error
     *
     * @param string $id Email address ID
     * @param string $address Valid Email address
     * @param bool $primary
     * @param bool $replyTo
     * @return bool
     */
    protected function handleEmailSaveAtUserProfile($id, $address, $primary, $replyTo)
    {
        global $current_user;

        // first validations

        if (!$id) {
            $GLOBALS['log']->error("Missing email ID");
            return false;
        }

        if (!$address) {
            $GLOBALS['log']->error("Missing email address");
            return false;
        }

        if (!$this->isValidEmail($address)) {
            $GLOBALS['log']->error("Invalid email address format");
            return false;
        }

        $email = new SugarEmailAddress();
        if (!$email->retrieve($id)) {
            $GLOBALS['log']->error('Email retrieve error, please ensure that the email ID is correct');
            return false;
        }

        $db = DBManagerFactory::getInstance();
        $query = sprintf("SELECT * FROM email_addresses WHERE id = %s AND deleted = 0", $db->quoted($id));
        if ($db->getOne($query) === false) {
            $GLOBALS['log']->error("Missing Email ID ($id)");
            return false;
        }

        // update primary and replyTo
        $_primary = (bool)$primary ? '1' : '0';
        $_replyTo = (bool)$replyTo ? '1' : '0';
        $_id = $db->quoted($id);
        $query =
            "UPDATE email_addr_bean_rel 
              SET 
                primary_address = '{$_primary}', 
                reply_to_address = '{$_replyTo}' 
              WHERE 
                email_address_id = {$_id} AND             
                bean_module = 'Users' AND 
                bean_id = '{$current_user->id}' AND
                deleted = 0";
        $result = $db->query($query);
        if (!$result) {
            $GLOBALS['log']->warn("Undefined behavior: Missing error information about email save");
        }
        if ($db->getAffectedRowCount($result) != 1) {
            $GLOBALS['log']->debug("Primary or reply-to Email address has not change");
        }

        return true;
    }


    /**
     * Check a valid email format,
     * return false if the email validation failed
     *
     * @param string $email
     * @return mixed
     */
    protected function isValidEmail($email)
    {
        $return = filter_var($email, FILTER_VALIDATE_EMAIL);
        return $return;
    }


    /**
     * Check for User Profile EditView / Save action for
     * Email Addresses updates
     * returns:
     * true - User Profile Save action called by request
     *
     * @param array $request $_REQUEST
     * @return bool
     */
    protected function isUserProfileEditViewPageSaveAction($request)
    {
        $return =
            (isset($request['page']) && $request['page'] == 'EditView') &&
            (isset($request['module']) && $request['module'] == 'Users') &&
            (isset($request['action']) && $request['action'] == 'Save');

        return $return;
    }


    /**
     * Fills standard email1 legacy fields
     * @param string id
     * @param string module
     * @return object
     */
    public function handleLegacyRetrieve(&$bean)
    {
        $module_dir = $this->getCorrectedModule($bean->module_dir);
        $this->addresses = $this->getAddressesByGUID($bean->id, $module_dir);
        $this->populateLegacyFields($bean);

        if (empty($bean->fetched_row)){
            return;
        }

        if ($bean->fetched_row === false) {
            $bean->fetched_row = [];
        }

        if (isset($bean->email1) && !isset($bean->fetched_row['email1'])) {
            $bean->fetched_row['email1'] = $bean->email1;
        }

        return;
    }

    public function populateLegacyFields(&$bean)
    {
        $primary_found = false;
        $alternate_found = false;
        $alternate2_found = false;
        foreach ($this->addresses as $k => $address) {
            if ($primary_found && $alternate_found) {
                break;
            }
            if ($address['primary_address'] == 1 && !$primary_found) {
                $primary_index = $k;
                $primary_found = true;
            } elseif (!$alternate_found) {
                $alternate_index = $k;
                $alternate_found = true;
            } elseif (!$alternate2_found) {
                $alternate2_index = $k;
                $alternate2_found = true;
            }
        }

        if ($primary_found) {
            $bean->email1 = $this->addresses[$primary_index]['email_address'];
            $bean->email_opt_out = $this->addresses[$primary_index]['opt_out'];
            $bean->invalid_email = $this->addresses[$primary_index]['invalid_email'];
            if ($alternate_found) {
                $bean->email2 = $this->addresses[$alternate_index]['email_address'];
            }
        } elseif ($alternate_found) {
            // Use the first found alternate as email1.
            $bean->email1 = $this->addresses[$alternate_index]['email_address'];
            $bean->email_opt_out = $this->addresses[$alternate_index]['opt_out'];
            $bean->invalid_email = $this->addresses[$alternate_index]['invalid_email'];
            if ($alternate2_found) {
                $bean->email2 = $this->addresses[$alternate2_index]['email_address'];
            }
        }
    }

    /**
     * @deprecated
     * @param bool $check_notify
     * @return null
     */
    public function save($check_notify = false)
    {
        $deprecatedMessage = 'SugarEmailAddress::save() function calls are deprecated use SugarEmailAddress::saveEmail() function instead';
        if (isset($GLOBALS['log'])) {
            $GLOBALS['log']->deprecated($deprecatedMessage);
        } else {
            trigger_error($deprecatedMessage, E_USER_DEPRECATED);
        }

        list($id, $module, $new_addrs, $primary, $replyTo, $invalid, $optOut, $in_workflow) = func_get_args();

        return $this->saveEmail($id, $module, $new_addrs, $primary, $replyTo, $invalid, $optOut, $in_workflow);
    }

    /**
     * Saves email addresses for a parent bean.
     * The base class SugarBean::save($check_notify) method is never called from SugarEmailAddresses::saveEmail(...)
     * The method's signature has been changed to correctly represent the save method call for SugarEmailAddress.
     * @param string $id Parent bean ID
     * @param string $module Parent bean's module
     * @param array $new_addrs Override of $_REQUEST vars, used to handle non-standard bean saves
     * @param string $primary GUID of primary address
     * @param string $replyTo GUID of reply-to address
     * @param string $invalid GUID of invalid address
     * @param string $optOut
     * @param bool $in_workflow
     * @param bool|null $opt_in
     * @return null
     */
    public function saveEmail(
        $id,
        $module,
        $new_addrs = array(),
        $primary = '',
        $replyTo = '',
        $invalid = '',
        $optOut = '',
        $in_workflow = false,
        $optIn = null
    ) {
        if (gettype($id) == "boolean") {
            $GLOBALS['log']->fatal('SugarEmailAddress::saveEmail() Invalid arguments - Parent method SugarBean::save
            ($checknotify) is not implemented. Please pass the correct arguments into SugarEmailAddress::saveEmail()');
        }

        if (
            empty($this->addresses)
            || $in_workflow === true
        ) {
            $this->populateAddresses($id, $module, $new_addrs, $primary);
        }

        // handle the Employee/User split
        $module = $this->getCorrectedModule($module);

        // find all email addresses
        $current_links = array();
        $q2 = "SELECT *  FROM email_addr_bean_rel eabr WHERE eabr.bean_id = '" . $this->db->quote($id) . "' AND eabr.bean_module = '" . $this->db->quote($module) . "' AND eabr.deleted=0";
        $r2 = $this->db->query($q2);
        while (($row2 = $this->db->fetchByAssoc($r2)) != null) {
            $current_links[$row2['email_address_id']] = $row2;
        }

        $isConversion = (isset($_REQUEST) && isset($_REQUEST['action']) && $_REQUEST['action'] == 'ConvertLead') ? true : false;

        if (!empty($this->addresses)) {
            // insert new relationships and create email address record, if they don't exist
            foreach ($this->addresses as $address) {
                if (!empty($address['email_address'])) {
                    $guid = create_guid();
                    $emailId = isset($address['email_address_id'])
                    && isset($current_links[$address['email_address_id']])
                        ? $address['email_address_id'] : null;
                    $emailId = $this->AddUpdateEmailAddress(
                        $address['email_address'],
                        $address['invalid_email'],
                        $address['opt_out'],
                        $emailId,
                        !is_null($optIn) ? $address['confirm_opt_in_flag'] : null
                    );// this will save the email address if not found

                    //verify linkage and flags.
                    $upd_eabr = "";
                    if (isset($current_links[$emailId])) {
                        if (!$isConversion) { // do not update anything if this is for lead conversion
                            if ($address['primary_address'] != $current_links[$emailId]['primary_address'] or $address['reply_to_address'] != $current_links[$emailId]['reply_to_address']) {
                                $upd_eabr = "UPDATE email_addr_bean_rel SET primary_address='" . $this->db->quote($address['primary_address']) . "', reply_to_address='" . $this->db->quote($address['reply_to_address']) . "' WHERE id='" . $this->db->quote($current_links[$emailId]['id']) . "'";
                            }

                            unset($current_links[$emailId]);
                        }
                    } else {
                        $primary = $address['primary_address'];
                        if (!empty($current_links) && $isConversion) {
                            foreach ($current_links as $eabr) {
                                if ($eabr['primary_address'] == 1) {
                                    // for lead conversion, if there is already a primary email, do not insert another primary email
                                    $primary = 0;
                                    break;
                                }
                            }
                        }
                        $now = $this->db->now();
                        $upd_eabr = "INSERT INTO email_addr_bean_rel (id, email_address_id,bean_id, bean_module,primary_address,reply_to_address,date_created,date_modified,deleted) VALUES('" . $this->db->quote($guid) . "', '" . $this->db->quote($emailId) . "', '" . $this->db->quote($id) . "', '" . $this->db->quote($module) . "', " . (int)$primary . ", " . (int)$address['reply_to_address'] . ", $now, $now, 0)";
                    }

                    if (!empty($upd_eabr)) {
                        $r2 = $this->db->query($upd_eabr);
                    }
                }
            }
        }

        //delete link to dropped email address.
        // for lead conversion, do not delete email addresses
        if (!empty($current_links) && !$isConversion) {
            $delete = "";
            foreach ($current_links as $eabr) {
                $delete .= empty($delete) ? "'" . $this->db->quote($eabr['id']) . "' " : ",'" . $this->db->quote($eabr['id']) . "'";
            }

            $eabr_unlink = "update email_addr_bean_rel set deleted=1 where id in ({$delete})";
            $this->db->query($eabr_unlink);
        }
        $this->stateBeforeWorkflow = null;
    }

    /**
     * returns the number of email addresses found for a specifed bean
     *
     * @param  string $email Address to match
     * @param  SugarBean $bean Bean to query against
     * @param  string $addressType Optional, pass a 1 to query against the primary address, 0 for the other addresses
     * @return int                 Count of records found
     * @throws \InvalidArgumentException
     */
    public function getCountEmailAddressByBean(
        $email,
        $bean,
        $addressType
    ) {
        $addressTypeInt = (int)$addressType;
        if ($addressType != 0 && $addressType != 1) {
            throw new InvalidArgumentException(
                'Invalid Address Type Argument: ' .
                'pass a 1 to query against the primary address, 0 for the other addresses'
            );
        }
        $emailCaps = strtoupper(trim($email));
        if (empty($emailCaps)) {
            return 0;
        }

        $q = "SELECT *
                FROM email_addr_bean_rel eabl JOIN email_addresses ea
                        ON (ea.id = eabl.email_address_id)
                    JOIN {$bean->table_name} bean
                        ON (eabl.bean_id = bean.id)
                WHERE ea.email_address_caps = '" . $this->db->quote($emailCaps) . "'
                    and eabl.bean_module = '" . $this->db->quote($bean->module_dir) . "'
                    and eabl.primary_address = '" . $this->db->quote($addressTypeInt) . "'
                    and eabl.deleted=0 ";

        $r = $this->db->query($q);

        // do it this way to make the count accurate in oracle
        $i = 0;
        while ($this->db->fetchByAssoc($r)) {
            ++$i;
        }

        return $i;
    }

    /**
     * This function returns a contact or user ID if a matching email is found
     * @param   string $email      the email address to match
     * @param   string $table      which table to query
     */
    public function getRelatedId($email, $module)
    {
        $email = $this->db->quote(trim(strtoupper($email)));
        $module = $this->db->quote(ucfirst($module));

        $q = "SELECT bean_id FROM email_addr_bean_rel eabr
                JOIN email_addresses ea ON (eabr.email_address_id = ea.id)
                WHERE bean_module = '$module' AND ea.email_address_caps = '$email' AND eabr.deleted=0";

        $r = $this->db->query($q, true);

        $returnArray = array();
        while ($a = $this->db->fetchByAssoc($r)) {
            $returnArray[] = $a['bean_id'];
        }
        if (count($returnArray) > 0) {
            return $returnArray;
        } else {
            return false;
        }
    }

    /**
     * returns a collection of beans matching the email address
     * @param string $email Address to match
     * @return array
     */
    public function getBeansByEmailAddress($email)
    {
        global $beanList;
        global $beanFiles;

        $return = array();

        $email = trim($email);

        if (empty($email)) {
            return array();
        }

        $emailCaps = "'" . $this->db->quote(strtoupper($email)) . "'";
        $q = "SELECT * FROM email_addr_bean_rel eabl JOIN email_addresses ea ON (ea.id = eabl.email_address_id and ea.deleted = 0)
                WHERE ea.email_address_caps = $emailCaps and eabl.deleted=0 ";
        $r = $this->db->query($q);

        while ($a = $this->db->fetchByAssoc($r)) {
            if (isset($beanList[$a['bean_module']]) && !empty($beanList[$a['bean_module']])) {
                $className = $beanList[$a['bean_module']];

                if (isset($beanFiles[$className]) && !empty($beanFiles[$className])) {
                    if (!class_exists($className)) {
                        require_once($beanFiles[$className]);
                    }

                    $bean = BeanFactory::getBean($a['bean_module'], $a['bean_id']);
                    if ($bean !== false) {
                        $return[] = $bean;
                    }

                } else {
                    $GLOBALS['log']->fatal("SUGAREMAILADDRESS: could not find valid class file for [ {$className} ]");
                }
            } else {
                $GLOBALS['log']->fatal("SUGAREMAILADDRESS: could not find valid class [ {$a['bean_module']} ]");
            }
        }

        return $return;
    }

    /**
     * Saves email addresses for a parent bean
     * @param string $id Parent bean ID
     * @param string $module  Parent bean's module
     * @param array $new_addrs Override of $_REQUEST vars, used to handle non-standard bean saves
     * @param string $primary GUID of primary address
     * @param string $replyTo GUID of reply-to address
     */
    public function populateAddresses(
        $id,
        $module,
        $new_addrs = array(),
        $primary = '',
        $replyTo = '',
        $invalid = '',
        $optOut = ''
    ) {
        if (!is_array($new_addrs)) {
            $GLOBALS['log']->fatal(
                'Invalid Argument: new address should be an array of strings, ' .
                gettype($new_addrs) . ' given.'
            );
        }
        $module = $this->getCorrectedModule($module);
        //One last check for the ConvertLead action in which case we need to change $module to 'Leads'
        $module = (isset($_REQUEST) && isset($_REQUEST['action']) && $_REQUEST['action'] === 'ConvertLead') ? 'Leads' : $module;

        $post_from_email_address_widget = (isset($_REQUEST[$module . '_email_widget_id']));
        $primaryValue = $primary;
        $widgetCount = 0;
        $widget_id = '';
        $hasEmailValue = false;
        $email_ids = array();

        if (isset($_REQUEST[$module . '_email_widget_id'])) {
            $fromRequest = false;
            // determine which array to process
            foreach ($_REQUEST as $k => $v) {
                if (strpos($k, 'emailAddress') !== false) {
                    $fromRequest = true;
                    break;
                }
                $widget_id = $_REQUEST[$module . '_email_widget_id'];
            }


            if (empty($widget_id)) {
                $GLOBALS['log']->debug('Widget not found, so it should be an update and not a create');
            }

            //Iterate over the widgets for this module, in case there are multiple email widgets for this module
            while (isset($_REQUEST[$module . $widget_id . 'emailAddress' . $widgetCount])) {
                if (empty($_REQUEST[$module . $widget_id . 'emailAddress' . $widgetCount])) {
                    $widgetCount++;
                    continue;
                }

                $hasEmailValue = true;

                $eId = $module . $widget_id;
                if (isset($_REQUEST[$eId . 'emailAddressPrimaryFlag'])) {
                    $primaryValue = $_REQUEST[$eId . 'emailAddressPrimaryFlag'];
                } elseif (isset($_REQUEST[$module . 'emailAddressPrimaryFlag'])) {
                    $primaryValue = $_REQUEST[$module . 'emailAddressPrimaryFlag'];
                }

                $optOutValues = array();
                if (isset($_REQUEST[$eId . 'emailAddressOptOutFlag'])) {
                    $optOutValues = $_REQUEST[$eId . 'emailAddressOptOutFlag'];
                } elseif (isset($_REQUEST[$module . 'emailAddressOptOutFlag'])) {
                    $optOutValues = $_REQUEST[$module . 'emailAddressOptOutFlag'];
                }

                $invalidValues = array();
                if (isset($_REQUEST[$eId . 'emailAddressInvalidFlag'])) {
                    $invalidValues = $_REQUEST[$eId . 'emailAddressInvalidFlag'];
                } elseif (isset($_REQUEST[$module . 'emailAddressInvalidFlag'])) {
                    $invalidValues = $_REQUEST[$module . 'emailAddressInvalidFlag'];
                }


                $optInValues = array();
                if (isset($_REQUEST[$eId . 'emailAddressOptInFlag'])) {
                    $optInValues = $_REQUEST[$eId . 'emailAddressOptInFlag'];
                } elseif (isset($_REQUEST[$module . 'emailAddressOptInFlag'])) {
                    $optInValues = $_REQUEST[$module . 'emailAddressOptInFlag'];
                }

                $deleteValues = array();
                if (isset($_REQUEST[$eId . 'emailAddressDeleteFlag'])) {
                    $deleteValues = $_REQUEST[$eId . 'emailAddressDeleteFlag'];
                } elseif (isset($_REQUEST[$module . 'emailAddressDeleteFlag'])) {
                    $deleteValues = $_REQUEST[$module . 'emailAddressDeleteFlag'];
                }

                // prep from form save
                $replyToField = '';
                $invalidField = '';
                $optOutField = '';
                $optInField = '';
                if ($fromRequest && empty($primary) && isset($primaryValue)) {
                    $primaryField = $primaryValue;
                }

                if ($fromRequest && empty($replyTo)) {
                    if (isset($_REQUEST[$eId . 'emailAddressReplyToFlag'])) {
                        $replyToField = $_REQUEST[$eId . 'emailAddressReplyToFlag'];
                    } elseif (isset($_REQUEST[$module . 'emailAddressReplyToFlag'])) {
                        $replyToField = $_REQUEST[$module . 'emailAddressReplyToFlag'];
                    }
                }
                if ($fromRequest && empty($new_addrs)) {
                    foreach ($_REQUEST as $k => $v) {
                        if (preg_match('/' . $eId . 'emailAddress[0-9]+$/i', $k) && !empty($v)) {
                            $new_addrs[$k] = $v;
                        }
                    }
                }
                if ($fromRequest && empty($email_ids)) {
                    foreach ($_REQUEST as $k => $v) {
                        if (preg_match('/' . $eId . 'emailAddressId[0-9]+$/i', $k) && !empty($v)) {
                            $key = str_replace('emailAddressId', 'emailAddress', $k);
                            $email_ids[$key] = $v;
                        }
                    }
                }

                // NOTE: probably it's never gonna happen:
                // $fromRequest became true if there is any emailAddress in request but
                // $new_addrs never empty because it's got a value if there is any emailAddress
                if ($fromRequest && empty($new_addrs)) {
                    foreach ($_REQUEST as $k => $v) {
                        if (preg_match('/' . $eId . 'emailAddressVerifiedValue[0-9]+$/i', $k) && !empty($v)) {
                            $validateFlag = str_replace("Value", "Flag", $k);
                            if (isset($_REQUEST[$validateFlag]) && $_REQUEST[$validateFlag] == "true") {
                                $new_addrs[$k] = $v;
                            }
                        }
                    }
                }

                //empty the addresses array if the post happened from email address widget.
                if ($post_from_email_address_widget) {
                    $this->addresses = array();  //this gets populated during retrieve of the contact bean.
                } else {
                    $optOutValues = array();
                    $invalidValues = array();
                    foreach ($new_addrs as $k => $email) {
                        preg_match('/emailAddress([0-9])+$/', $k, $matches);
                        $count = $matches[1];
                        $query = "SELECT opt_out, invalid_email, confirm_opt_in FROM email_addresses WHERE email_address_caps = '" . $this->db->quote(strtoupper($email)) . "'";
                        $result = $this->db->query($query);
                        if (!empty($result)) {
                            $row = $this->db->fetchByAssoc($result);
                            if (!empty($row['opt_out'])) {
                                $optOutValues[$k] = "emailAddress$count";
                            }

                            if (!empty($row['invalid_email'])) {
                                $invalidValues[$k] = "emailAddress$count";
                            }

                            if (!empty($row['confirm_opt_in'])) {
                                $optInValues[$k] = "emailAddress$count";
                            }
                        }
                    }
                }
                // Re-populate the addresses class variable if we have new address(es).
                if (!empty($new_addrs)) {
                    foreach ($new_addrs as $k => $reqVar) {
                        //$key = preg_match("/^$eId/s", $k) ? substr($k, strlen($eId)) : $k;
                        $reqVar = trim($reqVar);
                        if (strpos($k, 'emailAddress') !== false) {
                            if (!is_array($deleteValues)) {
                                $GLOBALS['log']->fatal('Invalid Argument: Delete Values to be an array, ' . gettype($deleteValues) . ' given.');
                            } else {
                                if (!empty($reqVar) && !in_array($k, $deleteValues)) {
                                    $email_id = (array_key_exists($k, $email_ids)) ? $email_ids[$k] : null;
                                    $primary = ($k == $primaryValue) ? true : false;
                                    $replyTo = ($k == $replyToField) ? true : false;
                                    $invalid = (in_array($k, (array)$invalidValues)) ? true : false;
                                    $optOut = (in_array($k, (array)$optOutValues)) ? true : false;
                                    $optIn = (in_array($k, $optInValues)) ? true : false;
                                    $this->addAddress(
                                        trim($new_addrs[$k]),
                                        $primary,
                                        $replyTo,
                                        $invalid,
                                        $optOut,
                                        $email_id,
                                        $optIn
                                    );
                                }
                            }
                        }
                    } //foreach
                }

                $widgetCount++;
            }//End of Widget for loop
        }

        //If no widgets, set addresses array to empty
        if ($post_from_email_address_widget && !$hasEmailValue) {
            $this->addresses = array();
        }
    }

    /**
     * Preps internal array structure for email addresses
     * @param string $addr Email address
     * @param bool $primary Default false
     * @param bool $replyTo Default false
     * @param bool $invalid Default false
     * @param bool $optOut Default false
     * @param string $email_id
     * @param bool $optIn Default false
     */
    public function addAddress(
        $addr,
        $primary = false,
        $replyTo = false,
        $invalid = false,
        $optOut = false,
        $email_id = null,
        $optIn = null
    ) {
        $addr = html_entity_decode($addr, ENT_QUOTES);
        if (preg_match($this->regex, $addr)) {
            $primaryFlag = ($primary) ? '1' : '0';
            $replyToFlag = ($replyTo) ? '1' : '0';
            $invalidFlag = ($invalid) ? '1' : '0';
            $optOutFlag = ($optOut) ? '1' : '0';
            if (!is_null($optIn)) {
                $optInFlag = ($optIn) ? '1' : '0';
            }

            $addr = trim($addr);

            // If we have such address already, remove it and add new one in.
            foreach ($this->addresses as $k => $address) {
                if ($address['email_address'] == $addr) {
                    unset($this->addresses[$k]);
                } elseif ($primary && $address['primary_address'] == '1') {
                    // We should only have one primary. If we are adding a primary but
                    // we find an existing primary, reset this one's primary flag.
                    $this->addresses[$k]['primary_address'] = '0';
                }
            }

            $addr = array(
                'email_address' => $addr,
                'primary_address' => $primaryFlag,
                'reply_to_address' => $replyToFlag,
                'invalid_email' => $invalidFlag,
                'opt_out' => $optOutFlag,
                'email_address_id' => $email_id,
                'confirm_opt_in_flag' => null,
            );

            if (!is_null($optIn)) {
                $addr['confirm_opt_in_flag'] = $optInFlag;
            }

            $this->addresses[] = $addr;
        } else {
            $GLOBALS['log']->fatal("SUGAREMAILADDRESS: address did not valid [ {$addr} ]");
        }
    }

    /**
     * Updates invalid_email and opt_out flags for each address
     */
    public function updateFlags()
    {
        if (!empty($this->addresses)) {
            foreach ($this->addresses as $addressMeta) {
                if (isset($addressMeta['email_address']) && !empty($addressMeta['email_address'])) {
                    $address = $this->db->quote($this->_cleanAddress($addressMeta['email_address']));

                    $q = "SELECT * FROM email_addresses WHERE email_address = '{$address}'";
                    $r = $this->db->query($q);
                    $a = $this->db->fetchByAssoc($r);

                    if (
                        !empty($a) &&
                        (
                            isset($a['invalid_email']) &&
                            isset($addressMeta['invalid_email']) &&
                            isset($addressMeta['opt_out']) &&
                            $a['invalid_email'] != $addressMeta['invalid_email'] ||
                            $a['opt_out'] != $addressMeta['opt_out']
                        )
                    ) {
                        $addressMetaInvalidEmailInt = (int)$addressMeta['invalid_email'];
                        $addressMetaOptOutInt = (int)$addressMeta['opt_out'];
                        $now = TimeDate::getInstance()->nowDb();
                        $id = $this->db->quote($a['id']);

                        $qUpdate = /** @lang sql */
                            "UPDATE email_addresses SET 
                              invalid_email = {$addressMetaInvalidEmailInt}, 
                              opt_out = {$addressMetaOptOutInt}, 
                              date_modified = '{$now}' 
                            WHERE id = '{$id}'";

                        $this->db->query($qUpdate);
                    }
                }
            }
        }
    }

    public function splitEmailAddress($addr)
    {
        if ($addr === null) {
            $addr = '';
        }

        $email = $this->_cleanAddress($addr);
        if (!preg_match($this->regex, $email)) {
            $email = ''; // remove bad email addr
        }
        $name = trim(str_replace(array($email, '<', '>', '"', "'"), '', $addr));

        return array("name" => $name, "email" => strtolower($email));
    }

    /**
     * PRIVATE UTIL
     * Normalizes an RFC-clean email address, returns a string that is the email address only
     * @param string $addr Dirty email address
     * @return string clean email address
     */
    public function _cleanAddress($addr)
    {
        $addr = trim(from_html($addr));

        if (strpos($addr, "<") !== false && strpos($addr, ">") !== false) {
            $address = trim(substr($addr, strrpos($addr, "<") + 1, strrpos($addr, ">") - strrpos($addr, "<") - 1));
        } else {
            $address = trim($addr);
        }

        return $address;
    }

    /**
     * preps a passed email address for email address storage
     * @param string $addr Address in focus, must be RFC compliant
     * @return string $id email_addresses ID
     */
    public function getEmailGUID($addr)
    {
        $address = $this->db->quote($this->_cleanAddress($addr));
        $addressCaps = strtoupper($address);

        $q = "SELECT id FROM email_addresses WHERE email_address_caps = '{$addressCaps}'";
        $r = $this->db->query($q);
        $a = $this->db->fetchByAssoc($r);

        if (!empty($a) && !empty($a['id'])) {
            return $a['id'];
        } else {
            $guid = '';
            if (!empty($address)) {
                $guid = create_guid();
                $now = TimeDate::getInstance()->nowDb();
                $qa = "INSERT INTO email_addresses (id, email_address, email_address_caps, date_created, date_modified, deleted)
                        VALUES('{$guid}', '{$address}', '{$addressCaps}', '$now', '$now', 0)";
                $ra = $this->db->query($qa);
            }

            return $guid;
        }
    }

    /**
     * Creates or Updates an entry in the email_addresses table, depending
     * on if the email address submitted matches a previous entry (case-insensitive)
     * @param string $addr - email address
     * @param int $invalid - is the email address marked as Invalid?
     * @param int $opt_out - is the email address marked as Opt-Out?
     * @param string $id - the GUID of the original SugarEmailAddress bean,
     *        in case a "email has changed" WorkFlow has triggered - hack to allow workflow-induced changes
     *        to propagate to the new SugarEmailAddress - see bug 39188
     * @param int|null $optInFlag
     * @return string GUID of Email Address or '' if cleaned address was empty.
     */
    public function AddUpdateEmailAddress($addr, $invalid = 0, $opt_out = 0, $id = null, $optInFlag = null)
    {
        // sanity checks to avoid SQL injection.
        $invalid = (int)$invalid;
        $opt_out = (int)$opt_out;

        $address = $this->db->quote($this->_cleanAddress($addr));
        $addressCaps = strtoupper($address);

        // determine if we have a matching email address
        $q = "SELECT * FROM email_addresses WHERE email_address_caps = '{$addressCaps}' and deleted=0";
        $r = $this->db->query($q);
        $duplicate_email = $this->db->fetchByAssoc($r);

        // check if we are changing an email address, where workflow might be in play
        if ($id) {
            $query = "SELECT * FROM email_addresses WHERE id='" . $this->db->quote($id) . "'";
            $r = $this->db->query($query);
            $current_email = $this->db->fetchByAssoc($r);
        } else {
            $current_email = null;
        }

        // unless workflow made changes, assume parameters are what to use.
        $new_opt_out = $opt_out;
        $new_invalid = $invalid;
        if (!empty($current_email['id']) && isset($this->stateBeforeWorkflow[$current_email['id']])) {
            if ($current_email['invalid_email'] != $invalid ||
                $current_email['opt_out'] != $opt_out
            ) {

                // workflow could be in play
                $before_email = $this->stateBeforeWorkflow[$current_email['id']];

                // our logic is as follows: choose from parameter, unless workflow made a change to the value, then choose final value
                if ((int)$before_email['opt_out'] != (int)$current_email['opt_out']) {
                    $new_opt_out = (int)$current_email['opt_out'];
                }
                if ((int)$before_email['invalid_email'] != (int)$current_email['invalid_email']) {
                    $new_invalid = (int)$current_email['invalid_email'];
                }
            }
        }

        // confirmed opt in check
        if (!is_null($optInFlag)) {
            $optInFlag = (int)$optInFlag;
        }

        $isValidEmailAddress = ($opt_out !== 1 && $invalid !== 1);
        $this->retrieve($id);
        $optInIndication = $this->getOptInStatus();
        if (
           $isValidEmailAddress
           && $this->isOptedInStatus($optInIndication)
           && (int)$optInFlag === 1
        ) {
            $new_confirmed_opt_in = $this->getConfirmedOptInState();
        } elseif (
            $isValidEmailAddress
            && (int)$optInFlag === 1
        ) {
            // In case optInFlag is set and there is a duplicate,
            // copy the opt-in state from it if it has some kind of opt-in set.
            // This prevents losing the confirmed opt-in state in case we
            // update an existing record with "confirmed-opt-in"
            if (!empty($duplicate_email['id']) && $duplicate_email['confirm_opt_in'] != self::COI_STAT_DISABLED) {
                $new_confirmed_opt_in = $duplicate_email['confirm_opt_in'];
            } else {
                $new_confirmed_opt_in = self::COI_STAT_OPT_IN;
            }
        } else {
            // Reset the opt in status
            $new_confirmed_opt_in = self::COI_STAT_DISABLED;
        }

        // determine how we are going to put in this address - UPDATE or INSERT
        if (!empty($duplicate_email['id'])) {
            $duplicate = clone $this;
            $duplicate->retrieve($duplicate_email['id']);
            // address_caps matches - see if we're changing fields
            if (
                $duplicate_email['invalid_email'] != $new_invalid
                || $duplicate_email['opt_out'] != $new_opt_out
                || (!is_null($optInFlag) && $duplicate_email['confirm_opt_in'] != $new_confirmed_opt_in)
                || (trim($duplicate_email['email_address']) != $address)
            ) {
                $upd_q = 'UPDATE ' . $this->table_name . ' ' .
                    'SET email_address=\'' . $address . '\', ' .
                    'invalid_email=' . $new_invalid . ', ' .
                    'opt_out=' . $new_opt_out . ', ' .
                    (!is_null($optInFlag) ? ('confirm_opt_in=\'' . $this->db->quote($new_confirmed_opt_in) . '\', ') : '') .
                    'date_modified=' . $this->db->now() . ' ' .
                    'WHERE id=\'' . $this->db->quote($duplicate_email['id']) . '\'';
                // set for audit table detection
                $duplicate->invalid_email = $new_invalid;
                $duplicate->opt_out = $new_opt_out;
                $duplicate->confirm_opt_in = $new_confirmed_opt_in;
                $upd_r = $this->db->query($upd_q);


                if ($new_confirmed_opt_in === self::COI_STAT_DISABLED) {
                    // reset confirm opt in
                    $upd_q = 'UPDATE ' . $this->table_name . ' ' .
                            'SET '.
                            'confirm_opt_in_date=NULL,' .
                            'confirm_opt_in_sent_date=NULL,' .
                            'confirm_opt_in_fail_date=NULL ' .
                            'WHERE id=\'' . $this->db->quote($duplicate_email['id']) . '\'';
                    $upd_r = $this->db->query($upd_q);
                    // set for audit table detection
                    $duplicate->confirm_opt_in = null;
                }
            }

            if (!empty($this->fetched_row)) {
                foreach ($this->fetched_row as $fieldName => $fieldValue) {
                    $this->{$fieldName} = $duplicate->{$fieldName};
                }
            }

            $this->auditBean(true);

            return $duplicate_email['id'];
        } else {
            // no case-insensitive address match - it's new, or undeleted.
            $guid = '';
            $isUpdate = true;
            if (!empty($address)) {
                $guid = create_guid();
                $now = TimeDate::getInstance()->nowDb();
                $qa = "INSERT INTO email_addresses (id, email_address, email_address_caps, date_created, date_modified, deleted, invalid_email, opt_out" . (!is_null($optInFlag) ? ", confirm_opt_in" : '') . ")
                        VALUES('{$guid}', '{$address}', '{$addressCaps}', '$now', '$now', 0 , $new_invalid, $new_opt_out" . (!is_null($optInFlag) ? ", '" . $this->db->quote($new_confirmed_opt_in) ."'" : '') . ")";
                $this->db->query($qa);
                $isUpdate = false;
            }

            $this->auditBean($isUpdate);
            return $guid;
        }
    }

    /**
     * @return string
     */
    public function getConfirmedOptInState()
    {
        return $this->confirm_opt_in;
    }

    /**
     * Returns Primary or newest email address
     * @param object $focus Object in focus
     * @return string email
     */
    public function getPrimaryAddress($focus, $parent_id = null, $parent_type = null)
    {
        $parent_type = empty($parent_type) ? $focus->module_dir : $parent_type;
        // Bug63174: Email address is not shown in the list view for employees
        $parent_type = $this->getCorrectedModule($parent_type);
        $parent_id = empty($parent_id) ? $focus->id : $parent_id;

        $q = "SELECT ea.email_address FROM email_addresses ea
                LEFT JOIN email_addr_bean_rel ear ON ea.id = ear.email_address_id
                WHERE ear.bean_module = '" . $this->db->quote($parent_type) . "'
                AND ear.bean_id = '" . $this->db->quote($parent_id) . "'
                AND ear.deleted = 0
                AND ea.invalid_email = 0
                ORDER BY ear.primary_address DESC";
        $r = $this->db->limitQuery($q, 0, 1);
        $a = $this->db->fetchByAssoc($r);

        if (isset($a['email_address'])) {
            return $a['email_address'];
        }

        return '';
    }

    /**
     * As long as this function is used not only to retrieve user's Reply-To
     * address, but also notification address and so on, there were added
     * $replyToOnly optional parameter used to retrieve only address marked as
     * Reply-To (bug #43643).
     *
     * @param SugarBean $focus
     * @param bool $replyToOnly
     * @return string
     */
    public function getReplyToAddress($focus, $replyToOnly = false)
    {
        $q = "SELECT ea.email_address FROM email_addresses ea
                LEFT JOIN email_addr_bean_rel ear ON ea.id = ear.email_address_id
                WHERE ear.bean_module = '" . $this->db->quote($focus->module_dir) . "'
                AND ear.bean_id = '" . $this->db->quote($focus->id) . "'
                AND ear.deleted = 0
                AND ea.invalid_email = 0";

        if (!$replyToOnly) {
            // retrieve reply-to address if it exists or any other address
            // otherwise
            $q .= "
                ORDER BY ear.reply_to_address DESC";
        } else {
            // retrieve reply-to address only
            $q .= "
                AND ear.reply_to_address = 1";
        }

        $r = $this->db->query($q);
        $a = $this->db->fetchByAssoc($r);

        if (isset($a['email_address'])) {
            return $a['email_address'];
        }

        return '';
    }

    /**
     * Returns all email addresses by parent's GUID
     * @param string $id Parent's GUID
     * @param string $module Parent's module
     * @return array
     */
    public function getAddressesByGUID($id, $module)
    {
        $return = array();
        $module = $this->getCorrectedModule($module);

        $q = "SELECT 
                    ea.email_address,
                    ea.email_address_caps,
                    ea.invalid_email,
                    ea.opt_out,
                    ea.confirm_opt_in,
                    ea.date_created,
                    ea.date_modified,
                    ear.id,
                    ear.email_address_id,
                    ear.bean_id,
                    ear.bean_module,
                    ear.primary_address,
                    ear.reply_to_address,
                    ear.deleted
                FROM email_addresses ea LEFT JOIN email_addr_bean_rel ear ON ea.id = ear.email_address_id
                WHERE 
                    ear.bean_module = '" . $this->db->quote($module) . "'
                    AND ear.bean_id = '" . $this->db->quote($id) . "'
                    AND ear.deleted = 0
                ORDER BY ear.reply_to_address, ear.primary_address DESC";
        $r = $this->db->query($q);

        while ($a = $this->db->fetchByAssoc($r, false)) {
            $return[] = $a;
        }

        return $return;
    }

    /**
     * Returns the HTML/JS for the EmailAddress widget
     * @global LoggerManager $log
     * @global array $app_strings
     * @global array $dictionary
     * @global array $beanList
     * @param string $parent_id ID of parent bean, generally $focus
     * @param string $module $focus' module
     * @param bool asMetadata Default false
     * @return string HTML/JS for widget
     */
    public function getEmailAddressWidgetEditView($id, $module, $asMetadata = false, $tpl = '', $tabindex = '0')
    {
        if (null === $id) {
            $GLOBALS['log']->debug('ID is null so it should be a create and NOT an update');
        }
        if (null === $module) {
            $GLOBALS['log']->fatal('Invalid Argument: module');

            return false;
        }

        if (!($this->smarty instanceof Sugar_Smarty)) {
            $this->smarty = new Sugar_Smarty();
        }

        global $app_strings;
        global $dictionary;
        global $beanList;
        $configurator = new Configurator();

        $prefill = 'false';

        $prefillData = 'new Object()';
        $passedModule = $module;
        $module = $this->getCorrectedModule($module);
        $saveModule = $module;
        if (isset($_POST['is_converted']) && $_POST['is_converted'] == true) {
            if (!isset($_POST['return_id'])) {
                $GLOBALS['log']->fatal('return_id not set');
                $id = null;
            } else {
                $id = $_POST['return_id'];
            }
            if (!isset($_POST['return_module'])) {
                $GLOBALS['log']->fatal('return_module not set');
                $module = '';
            } else {
                $module = $_POST['return_module'];
            }
        }
        $prefillDataArr = array();
        if (!empty($id)) {
            $prefillDataArr = $this->getAddressesByGUID($id, $module);
            //When coming from convert leads, sometimes module is Contacts while the id is for a lead.
            if (empty($prefillDataArr) && $module == "Contacts") {
                $prefillDataArr = $this->getAddressesByGUID($id, "Leads");
            }
        } elseif (isset($_REQUEST['full_form']) && !empty($_REQUEST['emailAddressWidget'])) {
            $widget_id = isset($_REQUEST[$module . '_email_widget_id']) ? $_REQUEST[$module . '_email_widget_id'] : '0';
            $count = 0;
            $key = $module . $widget_id . 'emailAddress' . $count;
            while (isset($_REQUEST[$key])) {
                $email = $_REQUEST[$key];
                $prefillDataArr[] = array(
                    'email_address' => $email,
                    'primary_address' => isset($_REQUEST['emailAddressPrimaryFlag']) && $_REQUEST['emailAddressPrimaryFlag'] == $key,
                    'invalid_email' => isset($_REQUEST['emailAddressInvalidFlag']) && in_array(
                        $key,
                        $_REQUEST['emailAddressInvalidFlag']
                    ),
                    'opt_out' => isset($_REQUEST['emailAddressOptOutFlag']) && in_array(
                        $key,
                        $_REQUEST['emailAddressOptOutFlag']
                    ),
                    'reply_to_address' => false
                );
                $key = $module . $widget_id . 'emailAddress' . ++$count;
            } //while
        }

        if (!empty($prefillDataArr)) {
            $json = new JSON();
            $prefillData = $json->encode($prefillDataArr);
            $prefill = !empty($prefillDataArr) ? 'true' : 'false';
        }

        $required = false;
        $moduleFound = true;
        if (!isset($beanList[$passedModule])) {
            $GLOBALS['log']->fatal('Module not found in bean list: ' . $passedModule);
            $moduleFound = false;
        } elseif (!isset($dictionary[$beanList[$passedModule]])) {
            $GLOBALS['log']->fatal('Module bean not found in dictionary: ' . $beanList[$passedModule]);
            $moduleFound = false;
        }
        if ($moduleFound) {
            $vardefs = $dictionary[$beanList[$passedModule]]['fields'];
        } else {
            return false;
        }
        if (!empty($vardefs['email1']) && isset($vardefs['email1']['required']) && $vardefs['email1']['required']) {
            $required = true;
        }
        $this->smarty->assign('required', $required);

        $this->smarty->assign('module', $saveModule);
        $this->smarty->assign('index', $this->index);
        $this->smarty->assign('app_strings', $app_strings);
        $this->smarty->assign('prefillEmailAddresses', $prefill);
        $this->smarty->assign('prefillData', $prefillData);
        $this->smarty->assign('tabindex', $tabindex);
        //Set addDefaultAddress flag (do not add if it's from the Email module)
        $this->smarty->assign(
            'addDefaultAddress',
            (isset($_REQUEST['module']) && $_REQUEST['module'] == 'Emails') ? 'false' : 'true'
        );
        $form = $this->view;

        //determine if this should be a quickcreate form, or a quick create form under subpanels
        if ($this->view == "QuickCreate") {
            // Fixed #1120 - fixed email validation for: Accounts -> Contacts subpanel -> Select -> Create Contact -> Save.
            // If email is required it should highlight this field and show an error message.
            // It didnt because the form was named form_DCSubpanelQuickCreate_Contacts instead of expected form_SubpanelQuickCreate_Contacts
            if ($this->object_name = 'EmailAddress' && $saveModule == 'Contacts') {
                $form = 'form_' . $this->view . '_' . $module;
            } else {
                $form = 'form_DC' . $this->view . '_' . $module;
            }
            if (isset($_REQUEST['action']) && (isset($_REQUEST['action']) && $_REQUEST['action'] == 'SubpanelCreates' || $_REQUEST['action'] == 'SubpanelEdits')) {
                $form = 'form_Subpanel' . $this->view . '_' . $module;
            }
        }

        $this->smarty->assign('emailView', $form);

        if ($module == 'Users') {
            $this->smarty->assign('useReplyTo', true);
        } else {
            $this->smarty->assign('useOptOut', true);
            $this->smarty->assign('useInvalid', true);
            if (
                $configurator->isOptInEnabled()
                || $configurator->isConfirmOptInEnabled()
            ) {
                $this->smarty->assign('useOptIn', true);
            } else {
                $this->smarty->assign('useOptIn', false);
            }
        }

        $template = empty($tpl) ? "include/SugarEmailAddress/templates/forEditView.tpl" : $tpl;
        $newEmail = $this->smarty->fetch($template);


        if ($asMetadata) {
            // used by Email 2.0
            $return = array();
            $return['prefillData'] = $prefillDataArr;
            $return['html'] = $newEmail;

            return $return;
        }

        return $newEmail;
    }


    /**
     * Returns the HTML/JS for the EmailAddress widget
     * @param object $focus Bean in focus
     * @return string HTML/JS for widget
     */
    public function getEmailAddressWidgetDetailView($focus, $tpl = '')
    {
        if (!($this->smarty instanceof Sugar_Smarty)) {
            $this->smarty = new Sugar_Smarty();
        }

        global $app_strings;
        global $current_user;
        $assign = array();
        if (empty($focus->id)) {
            return '';
        }
        $prefillData = $this->getAddressesByGUID($focus->id, $focus->module_dir);

        foreach ($prefillData as $addressItem) {
            $key = ($addressItem['primary_address'] == 1) ? 'primary' : '';
            $key = ($addressItem['reply_to_address'] == 1) ? 'reply_to' : $key;
            $key = ($addressItem['opt_out'] == 1) ? 'opt_out' : $key;
            $key = ($addressItem['invalid_email'] == 1) ? 'invalid' : $key;
            $key = ($addressItem['opt_out'] == 1) && ($addressItem['invalid_email'] == 1) ? 'opt_out_invalid' : $key;

            $emailAddress = array(
                'key' => $key,
                'address' => $current_user->getEmailLink2($addressItem['email_address'], $focus)
            );

            if (empty($emailAddress['address'])) {
                // Email Link is missing, lets just print the email address in plain text instead.
                $emailAddress['address'] = $addressItem['email_address'];
            }

            $assign[] =$emailAddress;
        }


        $this->smarty->assign('app_strings', $app_strings);
        $this->smarty->assign('emailAddresses', $assign);
        $templateFile = empty($tpl) ? "include/SugarEmailAddress/templates/forDetailView.tpl" : $tpl;
        $return = $this->smarty->fetch($templateFile);

        return $return;
    }


    /**
     * getEmailAddressWidgetDuplicatesView($focus)
     * @param object $focus Bean in focus
     * @return string HTML that contains hidden input values based off of HTML request
     */
    public function getEmailAddressWidgetDuplicatesView($focus)
    {
        if (!($this->smarty instanceof Sugar_Smarty)) {
            $this->smarty = new Sugar_Smarty();
        }

        $count = 0;
        $emails = array();
        $primary = null;
        $optOut = array();
        $invalid = array();
        $mod = isset($focus) ? $focus->module_dir : "";

        if (!isset($_POST) || !isset($_POST[$mod . '_email_widget_id'])) {
            $GLOBALS['log']->fatal("Missing Argument: a required post variable not found: {$mod}_email_widget_id");
            $widget_id = null;
        } else {
            $widget_id = $_POST[$mod . '_email_widget_id'];
        }
        $this->smarty->assign('email_widget_id', $widget_id);

        $emailAddressWidget = null;
        if (isset($_POST['emailAddressWidget'])) {
            $emailAddressWidget = $_POST['emailAddressWidget'];
        } else {
            $GLOBALS['log']->fatal('Missing Argument: a required post variable not found: emailAddressWidget');
        }

        $this->smarty->assign('emailAddressWidget', $emailAddressWidget);

        if (isset($_POST[$mod . $widget_id . 'emailAddressPrimaryFlag'])) {
            $primary = $_POST[$mod . $widget_id . 'emailAddressPrimaryFlag'];
        }

        while (isset($_POST[$mod . $widget_id . "emailAddress" . $count])) {
            $emails[] = $_POST[$mod . $widget_id . 'emailAddress' . $count];
            $count++;
        }

        if ($count == 0) {
            return "";
        }

        if (isset($_POST[$mod . $widget_id . 'emailAddressOptOutFlag'])) {
            if (
                !is_array($_POST[$mod . $widget_id . 'emailAddressOptOutFlag']) ||
                !is_object($_POST[$mod . $widget_id . 'emailAddressOptOutFlag'])
            ) {
                $GLOBALS['log']->fatal(
                    'Invalid Argument: post variable ' .
                    $mod . $widget_id . 'emailAddressOptOutFlag' .
                    ' should be an array, ' .
                    gettype($_POST[$mod . $widget_id . 'emailAddressOptOutFlag']) . ' given'
                );
            }
            foreach ((array)$_POST[$mod . $widget_id . 'emailAddressOptOutFlag'] as $v) {
                $optOut[] = $v;
            }
        }

        if (isset($_POST[$mod . $widget_id . 'emailAddressInvalidFlag'])) {
            if (
                !is_array($_POST[$mod . $widget_id . 'emailAddressInvalidFlag']) ||
                !is_object($_POST[$mod . $widget_id . 'emailAddressInvalidFlag'])
            ) {
                $GLOBALS['log']->fatal(
                    'Invalid Argument: post variable ' .
                    $mod . $widget_id . 'emailAddressInvalidFlag' .
                    ' should be an array, ' .
                    gettype($_POST[$mod . $widget_id . 'emailAddressInvalidFlag']) . ' given'
                );
            }
            foreach ((array)$_POST[$mod . $widget_id . 'emailAddressInvalidFlag'] as $v) {
                $invalid[] = $v;
            }
        }

        if (isset($_POST[$mod . $widget_id . 'emailAddressReplyToFlag'])) {
            if (
                !is_array($_POST[$mod . $widget_id . 'emailAddressReplyToFlag']) ||
                !is_object($_POST[$mod . $widget_id . 'emailAddressReplyToFlag'])
            ) {
                $GLOBALS['log']->fatal(
                    'Invalid Argument: post variable ' .
                    $mod . $widget_id . 'emailAddressReplyToFlag' .
                    ' should be an array, ' .
                    gettype($_POST[$mod . $widget_id . 'emailAddressReplyToFlag']) . ' given'
                );
            }
            foreach ((array)$_POST[$mod . $widget_id . 'emailAddressReplyToFlag'] as $v) {
                $replyTo[] = $v;
            }
        }

        if (isset($_POST[$mod . $widget_id . 'emailAddressDeleteFlag'])) {
            if (
                !is_array($_POST[$mod . $widget_id . 'emailAddressDeleteFlag']) ||
                !is_object($_POST[$mod . $widget_id . 'emailAddressDeleteFlag'])
            ) {
                $GLOBALS['log']->fatal(
                    'Invalid Argument: post variable ' .
                    $mod . $widget_id . 'emailAddressDeleteFlag' .
                    ' should be an array, ' .
                    gettype($_POST[$mod . $widget_id . 'emailAddressDeleteFlag']) . ' given'
                );
            }
            foreach ((array)$_POST[$mod . $widget_id . 'emailAddressDeleteFlag'] as $v) {
                $delete[] = $v;
            }
        }

        while (isset($_POST[$mod . $widget_id . "emailAddressVerifiedValue" . $count])) {
            if (
                !is_array($_POST[$mod . $widget_id . 'emailAddressVerifiedValue' . $count]) ||
                !is_object($_POST[$mod . $widget_id . 'emailAddressVerifiedValue' . $count])
            ) {
                $GLOBALS['log']->fatal(
                    'Invalid Argument: post variable ' .
                    $mod . $widget_id . 'emailAddressVerifiedValue' . $count .
                    ' not found.'
                );
            }
            $verified[] = $_POST[$mod . $widget_id . 'emailAddressVerifiedValue' . $count];
            $count++;
        }

        $this->smarty->assign('emails', $emails);
        $this->smarty->assign('primary', $primary);
        $this->smarty->assign('optOut', $optOut);
        $this->smarty->assign('invalid', $invalid);
        $this->smarty->assign('replyTo', $invalid);
        $this->smarty->assign('delete', $invalid);
        $this->smarty->assign('verified', $invalid);
        $this->smarty->assign('moduleDir', $mod);

        return $this->smarty->fetch("include/SugarEmailAddress/templates/forDuplicatesView.tpl");
    }

    /**
     * getFormBaseURL
     *
     */
    public function getFormBaseURL($focus)
    {
        $get = "";
        $count = 0;
        $mod = isset($focus) ? $focus->module_dir : "";

        if (!$mod) {
            $GLOBALS['log']->fatal('Invalid Argument: Missing module dir.');

            return false;
        }

        $widget_id = '';
        if (!isset($_POST[$mod . '_email_widget_id'])) {
            $GLOBALS['log']->fatal('Invalid Argument: requested argument missing: "' . $mod . '_email_widget_id"');
        } else {
            $widget_id = $_POST[$mod . '_email_widget_id'];
        }

        $get .= '&' . $mod . '_email_widget_id=' . $widget_id;

        if (!isset($_POST['emailAddressWidget'])) {
            $GLOBALS['log']->fatal('Invalid Argument: requested argument missing: "emailAddressWidget"');
            $get .= '&emailAddressWidget=';
        } else {
            $get .= '&emailAddressWidget=' . $_POST['emailAddressWidget'];
        }


        while (isset($_REQUEST[$mod . $widget_id . 'emailAddress' . $count])) {
            $get .= "&" . $mod . $widget_id . "emailAddress" . $count . "=" . urlencode($_REQUEST[$mod . $widget_id . 'emailAddress' . $count]);
            $count++;
        } //while

        while (isset($_REQUEST[$mod . $widget_id . 'emailAddressVerifiedValue' . $count])) {
            $get .= "&" . $mod . $widget_id . "emailAddressVerifiedValue" . $count . "=" . urlencode($_REQUEST[$mod . $widget_id . 'emailAddressVerifiedValue' . $count]);
            $count++;
        } //while

        $options = array(
            'emailAddressPrimaryFlag',
            'emailAddressOptOutFlag',
            'emailAddressInvalidFlag',
            'emailAddressDeleteFlag',
            'emailAddressReplyToFlag'
        );

        foreach ($options as $option) {
            $count = 0;
            $optionIdentifier = $mod . $widget_id . $option;
            if (isset($_REQUEST[$optionIdentifier])) {
                if (is_array($_REQUEST[$optionIdentifier])) {
                    foreach ($_REQUEST[$optionIdentifier] as $optOut) {
                        $get .= "&" . $optionIdentifier . "[" . $count . "]=" . $optOut;
                        $count++;
                    } //foreach
                } else {
                    $get .= "&" . $optionIdentifier . "=" . $_REQUEST[$optionIdentifier];
                }
            } //if
        } //foreach

        return $get;
    }

    public function setView($view)
    {
        $this->view = $view;
    }

    /**
     * This function is here so the Employees/Users division can be handled cleanly in one place
     * @param object $focus SugarBean
     * @return string The value for the bean_module column in the email_addr_bean_rel table
     */
    public function getCorrectedModule(&$module)
    {
        return ($module == "Employees") ? "Users" : $module;
    }

    public function stash($parentBeanId, $moduleName)
    {
        $result = $this->db->query("SELECT email_address_id FROM email_addr_bean_rel eabr WHERE eabr.bean_id = '" . $this->db->quote($parentBeanId) . "' AND eabr.bean_module = '" . $this->db->quote($moduleName) . "' AND eabr.deleted=0");
        $this->stateBeforeWorkflow = array();
        $ids = array();
        while ($row = $this->db->fetchByAssoc($result, false)) {
            $ids[] = $this->db->quote($row['email_address_id']); // avoid 2nd order SQL Injection
        }
        if (!empty($ids)) {
            $ids = implode("', '", $ids);
            $queryEmailData = "SELECT id, email_address, invalid_email, opt_out FROM {$this->table_name} WHERE id IN ('$ids') AND deleted=0";
            $result = $this->db->query($queryEmailData);
            while ($row = $this->db->fetchByAssoc($result, false)) {
                $this->stateBeforeWorkflow[$row['id']] = array_diff_key($row, array('id' => null));
            }
        }
    }


    /**
     * Confirm opt in
     */
    public function confirmOptIn()
    {
        $this->confirm_opt_in_date = TimeDate::getInstance()->nowDb();
        $this->confirm_opt_in = self::COI_STAT_CONFIRMED_OPT_IN;
    }

    /**
     * Reset opt in
     */
    public function resetOptIn()
    {
        $this->confirm_opt_in = self::COI_STAT_DISABLED;
        parent::save();
    }

    /**
     * Update Opt In state to SugarEmailAddress::COI_STAT_OPT_IN
     *
     * @see SugarEmailAddress::COI_STAT_OPT_IN
     * @return string|bool ID or false on failed
     * @throws RuntimeException this function updates an exists SugarEmailAddress bean should have ID
     */
    public function optIn()
    {
        if (!$this->id) {
            $msg = 'Trying to update opt-in email address without email address ID.';
            LoggerManager::getLogger()->fatal($msg);
            throw new RuntimeException($msg);
        }

        if (!$this->retrieve()) {
            $msg = 'Retrieve email address for opt-in failed.';
            LoggerManager::getLogger()->fatal($msg);
            throw new RuntimeException($msg);
        }

        $state = $this->isConfirmedOptIn() ? self::COI_STAT_CONFIRMED_OPT_IN : self::COI_STAT_OPT_IN;
        if (!$this->setConfirmedOptInState($state)) {
            $msg = 'set confirm opt in state of email address "' . $this->email_address . '" failed.';
            LoggerManager::getLogger()->fatal($msg);
            throw new RuntimeException($msg);
        }

        $ret = parent::save();


        return $ret;
    }

    /**
     *
     * @param string $state
     * @return boolean
     */
    public function setConfirmedOptInState($state)
    {
        $this->confirm_opt_in = $state;
        $ret = parent::save();
        return $ret;
    }

    /**
     * It returns a ViewDefs for Confirm Opt In action link on DetailViews, specially for Accounts/Contacts/Leads/Prospects
     *
     * @param string $module module name
     * @param string $returnModule optional, using module name if null
     * @param string $returnAction optional, using module name if null
     * @param string $moduleTab optional, using module name if null
     * @return array ViewDefs for Confirm Opt In action link
     */
    public static function getSendConfirmOptInEmailActionLinkDefs($module, $returnModule = null, $returnAction = null, $moduleTab = null)
    {
        $configurator = new Configurator();
        $configOptInEnabled = $configurator->isConfirmOptInEnabled();

        $disabledAttribute = $configOptInEnabled ? '' : 'disabled="disabled"';
        $hiddenClass = $configOptInEnabled ? '' : 'hidden';

        if (is_null($returnModule)) {
            $returnModule = $module;
        }

        if (is_null($returnAction)) {
            $returnAction = $module;
        }

        if (is_null($moduleTab)) {
            $moduleTab = $module;
        }

        $ret = array(
            'customCode' =>
            '<input type="submit" class="button ' .
            $hiddenClass . '" ' . $disabledAttribute .
            ' title="{$APP.LBL_SEND_CONFIRM_OPT_IN_EMAIL}" onclick="this.form.return_module.value=\'' .
            $returnModule . '\'; this.form.return_action.value=\'' .
            $returnAction . '\'; this.form.return_id.value=\'{$fields.id.value}\'; ' .
            'this.form.action.value=\'sendConfirmOptInEmail\'; this.form.module.value=\'' .
            $module . '\'; this.form.module_tab.value=\'' . $moduleTab .
            '\';" name="send_confirm_opt_in_email" value="{$APP.LBL_SEND_CONFIRM_OPT_IN_EMAIL}"/>',
            'sugar_html' =>
            array(
                'type' => 'submit',
                'value' => '{$APP.LBL_SEND_CONFIRM_OPT_IN_EMAIL}',
                'htmlOptions' =>
                array(
                    'class' => 'button ' . $hiddenClass,
                    'id' => 'send_confirm_opt_in_email',
                    'title' => '{$APP.LBL_SEND_CONFIRM_OPT_IN_EMAIL}',
                    'onclick' => 'this.form.return_module.value=\'' . $returnModule .
                    '\'; this.form.return_action.value=\'DetailView\'; this.form.return_id.value=\'{$fields.id.value}\'; ' .
                    'this.form.action.value=\'sendConfirmOptInEmail\'; this.form.module.value=\'' . $module .
                    '\'; this.form.module_tab.value=\'' . $moduleTab . '\';',
                    'name' => 'send_confirm_opt_in_email',
                ),
            ),
        );

        if (!$configOptInEnabled) {
            $ret['sugar_html']['htmlOptions']['disabled'] = true;
        }

        return $ret;
    }


    /**
     * Uses the configuration to determine opt in status
     * @return string
     */
    public function getOptInStatus()
    {
        $configurator = new Configurator();

        $enableConfirmedOptIn = null;
        if (isset($configurator->config['email_enable_confirm_opt_in'])) {
            $enableConfirmedOptIn = $configurator->config['email_enable_confirm_opt_in'];
        } else {
            LoggerManager::getLogger()->warn('EmailUI::populateComposeViewFields: $configurator->config[email_enable_confirm_opt_in] is not set');
        }

        $optInFromFlags = $this->getOptInIndicationFromFlags();

        if ($enableConfirmedOptIn === self::COI_STAT_DISABLED) {
            $ret = self::COI_FLAG_OPT_IN_DISABLED;
        } elseif (
            $enableConfirmedOptIn === self::COI_STAT_OPT_IN
            && $this->isOptedInStatus($optInFromFlags)
        ) {
            $ret = self::COI_FLAG_OPT_IN;
        } elseif ($enableConfirmedOptIn === self::COI_STAT_CONFIRMED_OPT_IN) {
            $ret = $optInFromFlags;
        } elseif ($optInFromFlags === self::COI_FLAG_INVALID) {
            $ret = $optInFromFlags;
        } elseif ($optInFromFlags === self::COI_FLAG_OPT_OUT) {
            $ret = $optInFromFlags;
        } else {
            $msg = 'Invalid ENUM value of Opt In settings: ' . $enableConfirmedOptIn;
            LoggerManager::getLogger()->warn($msg);
            $ret = self::COI_FLAG_NO_OPT_IN_STATUS;
        }

        return $ret;
    }

    /**
     * Determines the opt in status without considering the configuration
     * @return string
     * @throws  RuntimeException
     */
    private function getOptInIndicationFromFlags()
    {
        $log = LoggerManager::getLogger();

        if (!in_array($this->module_name, self::$doNotDisplayOptInTickForModule, true)) {
            if ((int)$this->invalid_email === 1) {
                $ret = self::COI_FLAG_INVALID;
                return $ret;
            }

            if ((int)$this->opt_out === 1) {
                $ret = self::COI_FLAG_OPT_OUT;
                return $ret;
            }

            if ($this->isNotOptIn()) {
                $ret = self::COI_STAT_DISABLED;
                return $ret;
            }

            $ret = self::COI_FLAG_UNKNOWN_OPT_IN_STATUS;

            if ($this->isConfirmedOptIn()) {
                $ret = self::COI_FLAG_OPT_IN_PENDING_EMAIL_CONFIRMED;
            } elseif (
                $this->isConfirmOptInEmailNotSent()
                && $this->getConfirmedOptInState() !== self::COI_STAT_DISABLED
            ) {
                $ret = self::COI_FLAG_OPT_IN_PENDING_EMAIL_NOT_SENT;
            } elseif ($this->isConfirmOptInEmailSent()) {
                $ret = self::COI_FLAG_OPT_IN_PENDING_EMAIL_SENT;
            } elseif ($this->isConfirmOptInEmailFailed()) {
                $ret = self::COI_FLAG_OPT_IN_PENDING_EMAIL_FAILED;
            }
            return $ret;
        }

        $ret = self::COI_FLAG_NO_OPT_IN_STATUS;
        return $ret;
    }


    /**
     * @return bool true when the an confirm optin email was successfully sent
     * @throws Exception
     */
    private function isConfirmOptInEmailSent()
    {
        if (empty($this->confirm_opt_in_sent_date)) {
            return false;
        }

        try {
            $maxdate = $this->dateMax($this->confirm_opt_in_sent_date, $this->confirm_opt_in_fail_date);
            if ($maxdate === $this->confirm_opt_in_fail_date) {
                return false;
            } elseif ($maxdate === $this->confirm_opt_in_sent_date) {
                return true;
            } else {
                throw new Exception('its impossible email sending state');
            }
        } catch (RuntimeException $e) {
            if (!empty($this->confirm_opt_in_fail_date)) {
                throw $e;
            }
            return true;
        }
    }

    /**
     * @return bool true when the an confirm optin email failed to send
     * @throws Exception
     */
    private function isConfirmOptInEmailFailed()
    {
        if (empty($this->confirm_opt_in_fail_date)) {
            return false;
        }

        try {
            $maxdate = $this->dateMax($this->confirm_opt_in_sent_date, $this->confirm_opt_in_fail_date);
            if ($maxdate === $this->confirm_opt_in_fail_date) {
                return true;
            } elseif ($maxdate === $this->confirm_opt_in_sent_date) {
                return false;
            } else {
                throw new Exception('its impossible email sending state');
            }
        } catch (RuntimeException $e) {
            if (!empty($this->confirm_opt_in_sent_date)) {
                throw $e;
            }
            return false;
        }
    }

    /**
     * @return bool if confirm opt in email has not yet been sent
     */
    private function isConfirmOptInEmailNotSent()
    {
        if (
            empty($this->confirm_opt_in_sent_date)
            && empty($this->confirm_opt_in_fail_date)
        ) {
            return true;
        }
        return false;
    }

    /**
     * @return bool true when confirmed opt in has been set
     */
    private function isConfirmedOptIn()
    {
        $ret =  $this->getConfirmedOptInState() === self::COI_STAT_CONFIRMED_OPT_IN;
        return $ret;
    }

    /**
     * @return bool
     */
    private function isNotOptIn()
    {
        return $this->confirm_opt_in === self::COI_STAT_DISABLED;
    }

    /**
     * @param string $date1
     * @param string $date2
     * @return bool
     * @throws \RuntimeException
     */
    private function dateCompare($date1, $date2)
    {
        $time1 = strtotime($date1);
        $time2 = strtotime($date2);

        $err = 'unable to convert strtotime: ';
        if ($time1 === -1) {
            throw new RuntimeException($err . $date1);
        }

        if ($time2 === -1) {
            throw new RuntimeException($err . $date2);
        }

        return $time1 > $time2;
    }


    /**
     * @param string $date1
     * @param string $date2
     * @return string
     */
    private function dateMax($date1, $date2)
    {
        return $this->dateCompare($date1, $date2) ? $date1 : $date2;
    }

    /**
     * @param string $emailAddressIndicatorStatus
     * @return bool
     */
    private function isOptedInStatus($emailAddressIndicatorStatus = self::COI_FLAG_NO_OPT_IN_STATUS)
    {
        $ret = in_array($emailAddressIndicatorStatus, array(
            self::COI_FLAG_OPT_IN_PENDING_EMAIL_CONFIRMED,
            self::COI_FLAG_OPT_IN_PENDING_EMAIL_SENT,
            self::COI_FLAG_OPT_IN_PENDING_EMAIL_NOT_SENT,
            self::COI_FLAG_OPT_IN_PENDING_EMAIL_FAILED,
        ), true);
        return $ret;
    }



    /**
     * @global array $app_strings
     * @return string
     */
    public function getOptInStatusTickHTML()
    {
        global $app_strings;

        $configurator = new Configurator();
        $sugar_config = $configurator->config;

        $tickHtml = '';

        if (isset($sugar_config['email_enable_confirm_opt_in'])) {
            $emailConfigEnableConfirmOptIn = $sugar_config['email_enable_confirm_opt_in'];

            $template = new Sugar_Smarty();

            $optInStatus = $this->getOptInStatus();
            switch ($optInStatus) {
                    case self::COI_FLAG_OPT_IN:
                        $optInFlagClass = 'email-opt-in-confirmed';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN'];
                        $optInFlagText = '<span class="suitepicon suitepicon-action-confirm"></span>';
                        break;
                    case self::COI_FLAG_OPT_IN_PENDING_EMAIL_CONFIRMED:
                        $optInFlagClass = 'email-opt-in-confirmed';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_CONFIRMED'];
                        $optInFlagText = '<span class="suitepicon suitepicon-action-confirm">';
                        $optInFlagText .= '</span><span class="suitepicon suitepicon-action-confirm"></span>';
                        break;
                    case self::COI_FLAG_OPT_IN_PENDING_EMAIL_SENT:
                        $optInFlagClass = 'email-opt-in-sent';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_PENDING_EMAIL_SENT'];
                        $optInFlagText = '<span class="suitepicon suitepicon-action-confirm"></span>';
                        break;
                    case self::COI_FLAG_OPT_IN_PENDING_EMAIL_NOT_SENT:
                        $optInFlagClass = 'email-opt-in-not-sent';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_PENDING_EMAIL_NOT_SENT'];
                        $optInFlagText = '<span class="suitepicon suitepicon-action-confirm"></span>';
                        break;
                    case self::COI_FLAG_OPT_IN_PENDING_EMAIL_FAILED:
                        $optInFlagClass = 'email-opt-in-failed';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_PENDING_EMAIL_FAILED'];
                        $optInFlagText = '<span class="suitepicon suitepicon-action-confirm"></span>';
                        break;
                    case self::COI_FLAG_OPT_OUT:
                        $optInFlagClass = 'email-opt-in-opt-out';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_OPT_OUT'];
                        $optInFlagText = '❌';
                        break;
                    case self::COI_FLAG_INVALID:
                        $optInFlagClass = 'email-opt-in-invalid';
                        $optInFlagTitle = $app_strings['LBL_OPT_IN_INVALID'];
                        $optInFlagText = '?';
                        break;
                    default:
                        $optInFlagClass = '';
                        $optInFlagTitle = '';
                        $optInFlagText = '';
                        break;
                }

            $template->assign('optInFlagClass', $optInFlagClass);
            $template->assign('optInFlagTitle', $optInFlagTitle);
            $template->assign('optInFlagText', $optInFlagText);
            $tickHtml = $template->fetch('include/SugarEmailAddress/templates/optInStatusTick.tpl');
        }

        return $tickHtml;
    }

    /**
     *
     * @return string
     */
    public function getConfirmOptInTokenGenerateIfNotExists()
    {
        if (!$this->confirm_opt_in_token) {
            $this->confirm_opt_in_token = md5(time() . md5($this->email_address) . md5(mt_rand(0, 9999999))) . md5(mt_rand(0, 9999999));
            $this->save();
        }
        return $this->confirm_opt_in_token;
    }
} // end class def

require_once __DIR__.'/getEmailAddressWidget.php';
