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; } // do we have to update the address? if ($email->email_address != $address) { $_address = $db->quote($address); $_addressCaps = $db->quote(strtoupper($address)); $_id = $db->quoted($id); $query = "UPDATE email_addresses SET email_address = '$_address', email_address_caps = '$_addressCaps' WHERE id = '{$_id}' AND deleted = 0"; $result = $db->query($query); if (!$result) { $GLOBALS['log']->warn("Undefined behavior: Missing error information about email save (1)"); } if ($db->getAffectedRowCount($result) != 1) { $GLOBALS['log']->debug("Email address has not change"); } } // 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 (2)"); } 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 (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) { $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' => '', '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 = ''; break; case self::COI_FLAG_OPT_IN_PENDING_EMAIL_CONFIRMED: $optInFlagClass = 'email-opt-in-confirmed'; $optInFlagTitle = $app_strings['LBL_OPT_IN_CONFIRMED']; $optInFlagText = ''; $optInFlagText .= ''; 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 = ''; 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 = ''; 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 = ''; 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';