From 838f61a948532eccaeabf14488f4398329c5c673 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 1 Dec 2009 22:02:03 +0000 Subject: [PATCH] Working email sending from fundraising module, and implement new emailqueue system. None of the existing emails are migrated to the new system yet, and both old and new work in parallel until we can finish migration --- Rmail/LICENSE.txt | 6 + Rmail/RFC822.php | 880 +++++++++++++++++++++ Rmail/Rmail.php | 1112 +++++++++++++++++++++++++++ Rmail/mimePart.php | 313 ++++++++ Rmail/smtp.php | 371 +++++++++ admin/communication.php | 222 +++++- admin/communication_send_status.php | 103 ++- admin/fundraising_campaigns.php | 9 +- admin/send_emailqueue.php | 98 +++ common.inc.php | 46 +- db/db.code.version.txt | 2 +- db/db.update.153.sql | 27 + 12 files changed, 3154 insertions(+), 35 deletions(-) create mode 100644 Rmail/LICENSE.txt create mode 100644 Rmail/RFC822.php create mode 100644 Rmail/Rmail.php create mode 100644 Rmail/mimePart.php create mode 100644 Rmail/smtp.php create mode 100644 admin/send_emailqueue.php create mode 100644 db/db.update.153.sql diff --git a/Rmail/LICENSE.txt b/Rmail/LICENSE.txt new file mode 100644 index 0000000..2d2731a --- /dev/null +++ b/Rmail/LICENSE.txt @@ -0,0 +1,6 @@ + License for Rmail + ================= + +This software is covered by the PHPGuru License. You can read it, along with a few FAQs, here: + + http://www.phpguru.org/static/license.html diff --git a/Rmail/RFC822.php b/Rmail/RFC822.php new file mode 100644 index 0000000..3f3d050 --- /dev/null +++ b/Rmail/RFC822.php @@ -0,0 +1,880 @@ + (A comment), ted@example.com (Ted Bloggs), Barney;'; +* $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', TRUE) +* print_r($structure); +*/ + +class Mail_RFC822 +{ + /** + * The address being parsed by the RFC822 object. + * @private string $address + */ + private $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @private string $default_domain + */ + private $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @private boolean $nestGroups + */ + private $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @private boolean $validate + */ + private $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @private array $addresses + */ + private $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @private array $structure + */ + private $structure = array(); + + /** + * The current error message, if any. + * @private string $error + */ + private $error = null; + + /** + * An internal counter/pointer. + * @private integer $index + */ + private $index = null; + + /** + * The number of groups that have been found in the address list. + * @private integer $num_groups + * @access public + */ + private $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @private boolean $mailRFC822 + */ + private $mailRFC822 = true; + + /** + * A limit after which processing stops + * @private int $limit + */ + private $limit = null; + + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + + if (!isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + while ($this->address = $this->_splitAddresses($this->address)) { + continue; + } + + if ($this->address === false || isset($this->error)) { + return false; + } + + // Reset timer since large amounts of addresses can take a long time to + // get here + set_time_limit(30); + + // Loop through all the addresses + for ($i = 0; $i < count($this->addresses); $i++){ + + if (($return = $this->_validateAddress($this->addresses[$i])) === false + || isset($this->error)) { + return false; + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $return); + } else { + $this->structure[] = $return; + } + } + + return $this->structure; + } + + /** + * Splits an address into seperate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + + if (!empty($this->limit) AND count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) + return false; + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has an unclosed quotes or not. + * + * @access private + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, false otherwise. + */ + function _hasUnclosedQuotes($string) + { + $string = explode('"', $string); + $string_cnt = count($string); + + for ($i = 0; $i < (count($string) - 1); $i++) + if (substr($string[$i], -1) == '\\') + $string_cnt--; + + return ($string_cnt % 2 === 0); + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!isset($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + for ($i = 0; $i < count($addresses); $i++) { + $addresses[$i] = trim($addresses[$i]); + } + + // Validate each mailbox. + // Format could be one of: name + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + array_walk($addresses, array($this, 'validateMailbox')); + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // Splits on one or more Tab or space. + $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + + $phrase_parts = array(); + while (count($parts) > 0){ + $phrase_parts[] = $this->_splitCheck($parts, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + for ($i = 0; $i < count($phrase_parts); $i++) { + // If quoted string: + if (substr($phrase_parts[$i], 0, 1) == '"') { + if (!$this->_validateQuotedString($phrase_parts[$i])) + return false; + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($phrase_parts[$i])) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1* + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // Validation has been turned off; assume the atom is okay. + return true; + } + + // Check for any char from ASCII 0 - ASCII 127 + if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { + return false; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check. + return !(preg_match('/(.)[\x0D\\\\"]/', $qstring, $matches) && $matches[1] != '\\'); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + + // Catch any RFC822 comments and store them separately + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a ( + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +1 is for the trailing ) + $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + } else { + break; + } + } + + for($i=0; $i' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) + return false; + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox,0,1) == '<' && substr($mailbox,-1) == '>') + $addr_spec = substr($mailbox,1,-1); + else + $addr_spec = $mailbox; + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) + return false; + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + for ($i = 0; $i < count($domains); $i++) { + $domains[$i] = str_replace('@', '', trim($domains[$i])); + if (!$this->_validateDomain($domains[$i])) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + for ($i = 0; $i < count($sub_domains); $i++) { + if (!$this->_validateSubdomain(trim($sub_domains[$i]))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + for ($i = 0; $i < count($words); $i++) { + if ($this->_validatePhrase(trim($words[$i])) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are + * in the given string. This is APPROXIMATE as it only splits + * based on a comma which has no preceding backslash. Could be + * useful as large amounts of addresses will end up producing + * *large* structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?@. This can be + * sufficient for most people. Optional stricter mode can + * be utilised which restricts mailbox characters allowed + * to alphanumeric, full stop, hyphen and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return false; + } + } +} + +?> diff --git a/Rmail/Rmail.php b/Rmail/Rmail.php new file mode 100644 index 0000000..96e6db5 --- /dev/null +++ b/Rmail/Rmail.php @@ -0,0 +1,1112 @@ +attachments = array(); + $this->html_images = array(); + $this->headers = array(); + $this->text = ''; + $this->sendmail_path = '/usr/lib/sendmail -ti'; + + /** + * If you want the auto load functionality + * to find other image/file types, add the + * extension and content type here. + */ + $this->image_types = array('gif' => 'image/gif', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'bmp' => 'image/bmp', + 'png' => 'image/png', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'swf' => 'application/x-shockwave-flash'); + + /** + * Set these up + */ + $this->build_params['html_encoding'] = new QPrintEncoding(); + $this->build_params['text_encoding'] = new SevenBitEncoding(); + $this->build_params['html_charset'] = 'ISO-8859-1'; + $this->build_params['text_charset'] = 'ISO-8859-1'; + $this->build_params['head_charset'] = 'ISO-8859-1'; + $this->build_params['text_wrap'] = 998; + + /** + * Defaults for smtp sending + */ + if (!empty($_SERVER['HTTP_HOST'])) { + $helo = $_SERVER['HTTP_HOST']; + + } elseif (!empty($_SERVER['SERVER_NAME'])) { + $helo = $_SERVER['SERVER_NAME']; + + } else { + $helo = 'localhost'; + } + + $this->smtp_params['host'] = 'localhost'; + $this->smtp_params['port'] = 25; + $this->smtp_params['helo'] = $helo; + $this->smtp_params['auth'] = false; + $this->smtp_params['user'] = ''; + $this->smtp_params['pass'] = ''; + + /** + * Make sure the MIME version header is first. + */ + $this->headers['MIME-Version'] = '1.0'; + $this->headers['X-Mailer'] = 'Rmail '; + } + + /** + * Accessor to set the CRLF style + * + * @param string $crlf CRLF style to use. + * Use \r\n for SMTP, and \n + * for normal. + */ + public function setCRLF($crlf = "\n") + { + if (!defined('RMAIL_CRLF')) { + define('RMAIL_CRLF', $crlf, true); + } + + if (!defined('MAIL_MIMEPART_CRLF')) { + define('MAIL_MIMEPART_CRLF', $crlf, true); + } + } + + /** + * Accessor to set the SMTP parameters + * + * @param string $host Hostname + * @param string $port Port + * @param string $helo HELO string to use + * @param bool $auth User authentication or not + * @param string $user Username + * @param string $pass Password + */ + public function setSMTPParams($host = null, $port = null, $helo = null, $auth = null, $user = null, $pass = null) + { + if (!is_null($host)) $this->smtp_params['host'] = $host; + if (!is_null($port)) $this->smtp_params['port'] = $port; + if (!is_null($helo)) $this->smtp_params['helo'] = $helo; + if (!is_null($auth)) $this->smtp_params['auth'] = $auth; + if (!is_null($user)) $this->smtp_params['user'] = $user; + if (!is_null($pass)) $this->smtp_params['pass'] = $pass; + } + + /** + * Sets sendmail path and options (optionally) (when directly piping to sendmail) + * + * @param string $path Path and options for sendmail command + */ + public function setSendmailPath($path) + { + $this->sendmail_path = $path; + } + + /** + * Accessor function to set the text encoding + * + * @param object $encoding Text encoding to use + */ + public function setTextEncoding(iEncoding $encoding) + { + $this->build_params['text_encoding'] = $encoding; + } + + /** + * Accessor function to set the HTML encoding + * + * @param object $encoding HTML encoding to use + */ + public function setHTMLEncoding(iEncoding $encoding) + { + $this->build_params['html_encoding'] = $encoding; + } + + /** + * Accessor function to set the text charset + * + * @param string $charset Character set to use + */ + public function setTextCharset($charset = 'ISO-8859-1') + { + $this->build_params['text_charset'] = $charset; + } + + /** + * Accessor function to set the HTML charset + * + * @param string $charset Character set to use + */ + public function setHTMLCharset($charset = 'ISO-8859-1') + { + $this->build_params['html_charset'] = $charset; + } + + /** + * Accessor function to set the header encoding charset + * + * @param string $charset Character set to use + */ + public function setHeadCharset($charset = 'ISO-8859-1') + { + $this->build_params['head_charset'] = $charset; + } + + /** + * Accessor function to set the text wrap count + * + * @param integer $count Point at which to wrap text + */ + public function setTextWrap($count = 998) + { + $this->build_params['text_wrap'] = $count; + } + + /** + * Accessor to set a header + * + * @param string $name Name of header + * @param string $value Value of header + */ + public function setHeader($name, $value) + { + $this->headers[$name] = $value; + } + + /** + * Sets the delivery reciept address + * + * @param string $email The address you want the delivery reciept + * sent to. Note that this is sent at the + * discretion of the recipient + */ + public function setReceipt($email) + { + $this->headers['Disposition-Notification-To'] = $email; + } + + /** + * Accessor to add a Subject: header + * + * @param string $subject Subject to set + */ + public function setSubject($subject) + { + $this->headers['Subject'] = $subject; + } + + /** + * Accessor to add a From: header + * + * @param string $from From address + */ + public function setFrom($from) + { + $this->headers['From'] = $from; + } + + /** + * Accessor to set priority. Priority given should be either + * high, normal or low. Can also be specified numerically, + * being 1, 3 or 5 (respectively). + * + * @param mixed $priority The priority to use. + */ + public function setPriority($priority = 'normal') + { + switch (strtolower($priority)) { + case 'high': + case '1': + $this->headers['X-Priority'] = '1'; + $this->headers['X-MSMail-Priority'] = 'High'; + break; + + case 'normal': + case '3': + $this->headers['X-Priority'] = '3'; + $this->headers['X-MSMail-Priority'] = 'Normal'; + break; + + case 'low': + case '5': + $this->headers['X-Priority'] = '5'; + $this->headers['X-MSMail-Priority'] = 'Low'; + break; + } + } + + /** + * Accessor to set the return path + * + * @param string $return_path Return path to use + */ + public function setReturnPath($return_path) + { + $this->return_path = $return_path; + } + + /** + * Accessor to add a Cc: header + * + * @param string $cc Carbon Copy address + */ + public function setCc($cc) + { + $this->headers['Cc'] = $cc; + } + + /** + * Accessor to add a Bcc: header + * + * @param string $bcc Blind Carbon Copy address + */ + public function setBcc($bcc) + { + $this->headers['Bcc'] = $bcc; + } + + /** + * Adds plain text. Use this function + * when NOT sending html email + * + * @param string $text Plain text of email + */ + public function setText($text) + { + $this->text = $text; + } + + /** + * Adds HTML to the emails, with an associated text part. + * If third part is given, images in the email will be loaded + * from this directory. + * + * @param string $html HTML part of email + * @param string $images_dir Images directory + */ + function setHTML($html, $images_dir = null) + { + $this->html = $html; + + if (!empty($images_dir)) { + $this->findHtmlImages($images_dir); + } + } + + /** + * Function for extracting images from + * html source. This function will look + * through the html code supplied by setHTML() + * and find any file that ends in one of the + * extensions defined in $obj->image_types. + * If the file exists it will read it in and + * embed it, (not an attachment). + * + * @param string $images_dir Images directory to look in + */ + private function findHtmlImages($images_dir) + { + // Build the list of image extensions + $extensions = array_keys($this->image_types); + + preg_match_all('/(?:"|\')([^"\']+\.('.implode('|', $extensions).'))(?:"|\')/Ui', $this->html, $matches); + + foreach ($matches[1] as $m) { + if (file_exists($images_dir . $m)) { + $html_images[] = $m; + $this->html = str_replace($m, basename($m), $this->html); + } + } + + /** + * Go thru found images + */ + if (!empty($html_images)) { + + // If duplicate images are embedded, they may show up as attachments, so remove them. + $html_images = array_unique($html_images); + sort($html_images); + + foreach ($html_images as $img) { + if ($image = file_get_contents($images_dir . $img)) { + $ext = preg_replace('#^.*\.(\w{3,4})$#e', 'strtolower("$1")', $img); + $content_type = $this->image_types[$ext]; + $this->addEmbeddedImage(new stringEmbeddedImage($image, basename($img), $content_type)); + } + } + } + } + + /** + * Adds an image to the list of embedded + * images. + * + * @param string $object Embedded image object + */ + public function addEmbeddedImage($embeddedImage) + { + $embeddedImage->cid = md5(uniqid(time())); + + $this->html_images[] = $embeddedImage; + } + + + /** + * Adds a file to the list of attachments. + * + * @param string $attachment Attachment object + */ + public function addAttachment($attachment) + { + $this->attachments[] = $attachment; + } + + /** + * Adds a text subpart to a mime_part object + * + * @param object $obj + * @return object Mime part object + */ + private function addTextPart(&$message) + { + $params['content_type'] = 'text/plain'; + $params['encoding'] = $this->build_params['text_encoding']->getType(); + $params['charset'] = $this->build_params['text_charset']; + + if (!empty($message)) { + $message->addSubpart($this->text, $params); + } else { + $message = new Mail_mimePart($this->text, $params); + } + } + + /** + * Adds a html subpart to a mime_part object + * + * @param object $obj + * @return object Mime part object + */ + private function addHtmlPart(&$message) + { + $params['content_type'] = 'text/html'; + $params['encoding'] = $this->build_params['html_encoding']->getType(); + $params['charset'] = $this->build_params['html_charset']; + + if (!empty($message)) { + $message->addSubpart($this->html, $params); + } else { + $message = new Mail_mimePart($this->html, $params); + } + } + + /** + * Starts a message with a mixed part + * + * @return object Mime part object + */ + private function addMixedPart(&$message) + { + $params['content_type'] = 'multipart/mixed'; + + $message = new Mail_mimePart('', $params); + } + + /** + * Adds an alternative part to a mime_part object + * + * @param object $obj + * @return object Mime part object + */ + private function addAlternativePart(&$message) + { + $params['content_type'] = 'multipart/alternative'; + + if (!empty($message)) { + return $message->addSubpart('', $params); + } else { + $message = new Mail_mimePart('', $params); + } + } + + /** + * Adds a html subpart to a mime_part object + * + * @param object $obj + * @return object Mime part object + */ + private function addRelatedPart(&$message) + { + $params['content_type'] = 'multipart/related'; + + if (!empty($message)) { + return $message->addSubpart('', $params); + } else { + $message = new Mail_mimePart('', $params); + } + } + + /** + * Adds all html images to a mime_part object + * + * @param object $obj Message object + */ + private function addHtmlImageParts(&$message) + { + foreach ($this->html_images as $value) { + $params['content_type'] = $value->contentType; + $params['encoding'] = $value->encoding->getType(); + $params['disposition'] = 'inline'; + $params['dfilename'] = $value->name; + $params['cid'] = $value->cid; + + $message->addSubpart($value->data, $params); + } + } + + /** + * Adds all attachments to a mime_part object + * + * @param object $obj Message object + */ + private function addAttachmentParts(&$message) + { + foreach ($this->attachments as $value) { + $params['content_type'] = $value->contentType; + $params['encoding'] = $value->encoding->getType(); + $params['disposition'] = 'attachment'; + $params['dfilename'] = $value->name; + + $message->addSubpart($value->data, $params); + } + } + + /** + * Builds the multipart message. + */ + private function build() + { + if (!empty($this->html_images)) { + foreach ($this->html_images as $value) { + $quoted = preg_quote($value->name); + $cid = preg_quote($value->cid); + + $this->html = preg_replace("#src=\"$quoted\"|src='$quoted'#", "src=\"cid:$cid\"", $this->html); + $this->html = preg_replace("#background=\"$quoted\"|background='$quoted'#", "background=\"cid:$cid\"", $this->html); + } + } + + $message = null; + $attachments = !empty($this->attachments); + $html_images = !empty($this->html_images); + $html = !empty($this->html); + $text = !$html; + + switch (true) { + case $text: + $message = null; + if ($attachments) { + $this->addMixedPart($message); + } + + $this->addTextPart($message); + + // Attachments + $this->addAttachmentParts($message); + break; + + case $html AND !$attachments AND !$html_images: + $this->addAlternativePart($message); + + $this->addTextPart($message); + $this->addHtmlPart($message); + break; + + case $html AND !$attachments AND $html_images: + $this->addRelatedPart($message); + $alt = $this->addAlternativePart($message); + + $this->addTextPart($alt); + $this->addHtmlPart($alt); + + // HTML images + $this->addHtmlImageParts($message); + break; + + case $html AND $attachments AND !$html_images: + $this->addMixedPart($message); + $alt = $this->addAlternativePart($message); + + $this->addTextPart($alt); + $this->addHtmlPart($alt); + + // Attachments + $this->addAttachmentParts($message); + break; + + case $html AND $attachments AND $html_images: + $this->addMixedPart($message); + $rel = $this->addRelatedPart($message); + $alt = $this->addAlternativePart($rel); + + $this->addTextPart($alt); + $this->addHtmlPart($alt); + + // HTML images + $this->addHtmlImageParts($rel); + + // Attachments + $this->addAttachmentParts($message); + break; + + } + + if (isset($message)) { + $output = $message->encode(); + $this->output = $output['body']; + $this->headers = array_merge($this->headers, $output['headers']); + + // Figure out hostname + if (!empty($_SERVER['HTTP_HOST'])) { + $hostname = $_SERVER['HTTP_HOST']; + + } else if (!empty($_SERVER['SERVER_NAME'])) { + $hostname = $_SERVER['SERVER_NAME']; + + } else if (!empty($_ENV['HOSTNAME'])) { + $hostname = $_ENV['HOSTNAME']; + + } else { + $hostname = 'localhost'; + } + + $message_id = sprintf('<%s.%s@%s>', base_convert(time(), 10, 36), base_convert(rand(), 10, 36), $hostname); + $this->headers['Message-ID'] = $message_id; + + return true; + } else { + return false; + } + } + + /** + * Function to encode a header if necessary + * according to RFC2047 + * + * @param string $input Value to encode + * @param string $charset Character set to use + * @return string Encoded value + */ + private function encodeHeader($input, $charset = 'ISO-8859-1') + { + preg_match_all('/(\w*[\x80-\xFF]+\w*)/', $input, $matches); + foreach ($matches[1] as $value) { + $replacement = preg_replace('/([\x80-\xFF])/e', '"=" . strtoupper(dechex(ord("\1")))', $value); + $input = str_replace($value, '=?' . $charset . '?Q?' . $replacement . '?=', $input); + } + + return $input; + } + + /** + * Sends the mail. + * + * @param array $recipients Array of receipients to send the mail to + * @param string $type How to send the mail ('mail' or 'sendmail' or 'smtp') + * @return mixed + */ + public function send($recipients, $type = 'mail') + { + if (!defined('RMAIL_CRLF')) { + $this->setCRLF( ($type == 'mail' OR $type == 'sendmail') ? "\n" : "\r\n"); + } + + $this->build(); + + switch ($type) { + case 'mail': + $subject = ''; + if (!empty($this->headers['Subject'])) { + $subject = $this->encodeHeader($this->headers['Subject'], $this->build_params['head_charset']); + unset($this->headers['Subject']); + } + + // Get flat representation of headers + foreach ($this->headers as $name => $value) { + $headers[] = $name . ': ' . $this->encodeHeader($value, $this->build_params['head_charset']); + } + + $to = $this->encodeHeader(implode(', ', $recipients), $this->build_params['head_charset']); + + if (!empty($this->return_path)) { + $result = mail($to, $subject, $this->output, implode(RMAIL_CRLF, $headers), '-f' . $this->return_path); + } else { + $result = mail($to, $subject, $this->output, implode(RMAIL_CRLF, $headers)); + } + + // Reset the subject in case mail is resent + if ($subject !== '') { + $this->headers['Subject'] = $subject; + } + + + // Return + return $result; + break; + + case 'sendmail': + // Get flat representation of headers + foreach ($this->headers as $name => $value) { + $headers[] = $name . ': ' . $this->encodeHeader($value, $this->build_params['head_charset']); + } + + // Encode To: + $headers[] = 'To: ' . $this->encodeHeader(implode(', ', $recipients), $this->build_params['head_charset']); + + // Get return path arg for sendmail command if necessary + $returnPath = ''; + if (!empty($this->return_path)) { + $returnPath = '-f' . $this->return_path; + } + + $pipe = popen($this->sendmail_path . " " . $returnPath, 'w'); + $bytes = fputs($pipe, implode(RMAIL_CRLF, $headers) . RMAIL_CRLF . RMAIL_CRLF . $this->output); + $r = pclose($pipe); + + return $r; + break; + + case 'smtp': + require_once(dirname(__FILE__) . '/smtp.php'); + require_once(dirname(__FILE__) . '/RFC822.php'); + $smtp = &smtp::connect($this->smtp_params); + + // Parse recipients argument for internet addresses + foreach ($recipients as $recipient) { + $addresses = Mail_RFC822::parseAddressList($recipient, $this->smtp_params['helo'], null, false); + foreach ($addresses as $address) { + $smtp_recipients[] = sprintf('%s@%s', $address->mailbox, $address->host); + } + } + unset($addresses); // These are reused + unset($address); // These are reused + + // Get flat representation of headers, parsing + // Cc and Bcc as we go + foreach ($this->headers as $name => $value) { + if ($name == 'Cc' OR $name == 'Bcc') { + $addresses = Mail_RFC822::parseAddressList($value, $this->smtp_params['helo'], null, false); + foreach ($addresses as $address) { + $smtp_recipients[] = sprintf('%s@%s', $address->mailbox, $address->host); + } + } + if ($name == 'Bcc') { + continue; + } + $headers[] = $name . ': ' . $this->encodeHeader($value, $this->build_params['head_charset']); + } + // Add To header based on $recipients argument + $headers[] = 'To: ' . $this->encodeHeader(implode(', ', $recipients), $this->build_params['head_charset']); + + // Add headers to send_params + $send_params['headers'] = $headers; + $send_params['recipients'] = array_values(array_unique($smtp_recipients)); + $send_params['body'] = $this->output; + + // Setup return path + if (isset($this->return_path)) { + $send_params['from'] = $this->return_path; + } elseif (!empty($this->headers['From'])) { + $from = Mail_RFC822::parseAddressList($this->headers['From']); + $send_params['from'] = sprintf('%s@%s', $from[0]->mailbox, $from[0]->host); + } else { + $send_params['from'] = 'postmaster@' . $this->smtp_params['helo']; + } + + // Send it + if (!$smtp->send($send_params)) { + $this->errors = $smtp->getErrors(); + return false; + } + return true; + break; + } + } + + /** + * Use this method to return the email + * in message/rfc822 format. Useful for + * adding an email to another email as + * an attachment. + * + * @param array $recipients Array of recipients + * @param string $type Method to be used to send the mail. + * Used to determine the line ending type. + */ + public function getRFC822($recipients, $type = 'mail') + { + // Make up the date header as according to RFC822 + $this->setHeader('Date', date('D, d M y H:i:s O')); + + if (!defined('RMAIL_CRLF')) { + $this->setCRLF($type == 'mail' ? "\n" : "\r\n"); + } + + $this->build(); + + // Return path ? + if (isset($this->return_path)) { + $headers[] = 'Return-Path: ' . $this->return_path; + } + + // Get flat representation of headers + foreach ($this->headers as $name => $value) { + $headers[] = $name . ': ' . $value; + } + $headers[] = 'To: ' . implode(', ', $recipients); + + return implode(RMAIL_CRLF, $headers) . RMAIL_CRLF . RMAIL_CRLF . $this->output; + } +} // End of class. + + +/** +* Attachment classes +*/ +class attachment +{ + /** + * Data of attachment + * @var string + */ + public $data; + + /** + * Name of attachment (filename) + * @var string + */ + public $name; + + /** + * Content type of attachment + * @var string + */ + public $contentType; + + /** + * Encoding type of attachment + * @var object + */ + public $encoding; + + /** + * Constructor + * + * @param string $data File data + * @param string $name Name of attachment (filename) + * @param string $contentType Content type of attachment + * @param object $encoding Encoding type to use + */ + public function __construct($data, $name, $contentType, iEncoding $encoding) + { + $this->data = $data; + $this->name = $name; + $this->contentType = $contentType; + $this->encoding = $encoding; + } +} + + +/** +* File based attachment class +*/ +class fileAttachment extends attachment +{ + /** + * Constructor + * + * @param string $filename Name of file + * @param string $contentType Content type of file + * @param string $encoding What encoding to use + */ + public function __construct($filename, $contentType = 'application/octet-stream', $encoding = null) + { + $encoding = is_null($encoding) ? new Base64Encoding() : $encoding; + + parent::__construct(file_get_contents($filename), basename($filename), $contentType, $encoding); + } +} + + +/** +* Attachment class to handle attachments which are contained +* in a variable. +*/ +class stringAttachment extends attachment +{ + /** + * Constructor + * + * @param string $data File data + * @param string $name Name of attachment (filename) + * @param string $contentType Content type of file + * @param string $encoding What encoding to use + */ + public function __construct($data, $name = '', $contentType = 'application/octet-stream', $encoding = null) + { + $encoding = is_null($encoding) ? new Base64Encoding() : $encoding; + + parent::__construct($data, $name, $contentType, $encoding); + } +} + + +/** +* File based embedded image class +*/ +class fileEmbeddedImage extends fileAttachment +{ +} + + +/** +* String based embedded image class +*/ +class stringEmbeddedImage extends stringAttachment +{ +} + + +/** +* +*/ +/** +* Encoding interface +*/ +interface iEncoding +{ + public function encode($input); + public function getType(); +} + + +/** +* Base64 Encoding class +*/ +class Base64Encoding implements iEncoding +{ + /* + * Function to encode data using + * base64 encoding. + * + * @param string $input Data to encode + */ + public function encode($input) + { + return rtrim(chunk_split(base64_encode($input), 76, defined('MAIL_MIME_PART_CRLF') ? MAIL_MIME_PART_CRLF : "\r\n")); + } + + /** + * Returns type + */ + public function getType() + { + return 'base64'; + } +} + + +/** +* Quoted Printable Encoding class +*/ +class QPrintEncoding implements iEncoding +{ + /* + * Function to encode data using + * quoted-printable encoding. + * + * @param string $input Data to encode + */ + public function encode($input) + { + // Replace non printables + $input = preg_replace('/([^\x20\x21-\x3C\x3E-\x7E\x0A\x0D])/e', 'sprintf("=%02X", ord("\1"))', $input); + $inputLen = strlen($input); + $outLines = array(); + $output = ''; + + $lines = preg_split('/\r?\n/', $input); + + // Walk through each line + for ($i=0; $i $lineMax) { + $outLines[] = substr($lines[$i], 0, $lineMax - 1) . "="; // \r\n Gets added when lines are imploded + $lines[$i] = substr($lines[$i], $lineMax - 1); + $i--; // Ensure this line gets redone as we just changed it + } else { + $outLines[] = $lines[$i]; + } + } + + // Convert trailing whitespace + $output = preg_replace('/(\x20+)$/me', 'str_replace(" ", "=20", "\1")', $outLines); + + return implode("\r\n", $output); + } + + /** + * Returns type + */ + public function getType() + { + return 'quoted-printable'; + } +} + + +/** +* 7Bit Encoding class +*/ +class SevenBitEncoding implements iEncoding +{ + /* + * Function to "encode" data using + * 7bit encoding. + * + * @param string $input Data to encode + */ + public function encode($input) + { + return $input; + } + + /** + * Returns type + */ + public function getType() + { + return '7bit'; + } +} + + +/** +* 8Bit Encoding class +*/ +class EightBitEncoding implements iEncoding +{ + /* + * Function to "encode" data using + * 8bit encoding. + * + * @param string $input Data to encode + */ + public function encode($input) + { + return $input; + } + + /** + * Returns type + */ + public function getType() + { + return '8bit'; + } +} +?> \ No newline at end of file diff --git a/Rmail/mimePart.php b/Rmail/mimePart.php new file mode 100644 index 0000000..813d7a9 --- /dev/null +++ b/Rmail/mimePart.php @@ -0,0 +1,313 @@ +addSubPart($body, $params); +* +* // Now add an attachment. Assume $attach is +* the contents of the attachment +* +* $params['content_type'] = 'application/zip'; +* $params['encoding'] = 'base64'; +* $params['disposition'] = 'attachment'; +* $params['dfilename'] = 'example.zip'; +* $attach =& $email->addSubPart($body, $params); +* +* // Now build the email. Note that the encode +* // function returns an associative array containing two +* // elements, body and headers. You will need to add extra +* // headers, (eg. Mime-Version) before sending. +* +* $email = $message->encode(); +* $email['headers'][] = 'Mime-Version: 1.0'; +* +* +* Further examples are available at http://www.phpguru.org +* +* TODO: +* - Set encode() to return the $obj->encoded if encode() +* has already been run. Unless a flag is passed to specifically +* re-build the message. +* +* @author Richard Heyes +* @version $Revision: 1.3 $ +* @package Mail +*/ + +class Mail_MIMEPart +{ + /** + * The encoding type of this part + * @var string + */ + private $encoding; + + /** + * An array of subparts + * @var array + */ + private $subparts; + + /** + * The output of this part after being built + * @var string + */ + private $encoded; + + /** + * Headers for this part + * @var array + */ + private $headers; + + /** + * The body of this part (not encoded) + * @var string + */ + private $body; + + /** + * Constructor. + * + * Sets up the object. + * + * @param $body - The body of the mime part if any. + * @param $params - An associative array of parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, base64, or quoted-printable + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * dfilename - Optional filename parameter for content disposition + * description - Content description + * charset - Character set to use + * @access public + */ + public function __construct($body = '', $params = array()) + { + if (!defined('MAIL_MIMEPART_CRLF')) { + define('MAIL_MIMEPART_CRLF', defined('MAIL_MIME_CRLF') ? MAIL_MIME_CRLF : "\r\n", true); + } + + foreach ($params as $key => $value) { + switch ($key) { + case 'content_type': + $headers['Content-Type'] = $value . (isset($charset) ? '; charset="' . $charset . '"' : ''); + break; + + case 'encoding': + $this->encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'disposition': + $headers['Content-Disposition'] = $value . (isset($dfilename) ? '; filename="' . $dfilename . '"' : ''); + break; + + case 'dfilename': + if (isset($headers['Content-Disposition'])) { + $headers['Content-Disposition'] .= '; filename="' . $value . '"'; + } else { + $dfilename = $value; + } + break; + + case 'description': + $headers['Content-Description'] = $value; + break; + + case 'charset': + if (isset($headers['Content-Type'])) { + $headers['Content-Type'] .= '; charset="' . $value . '"'; + } else { + $charset = $value; + } + break; + } + } + + // Default content-type + if (!isset($headers['Content-Type'])) { + $headers['Content-Type'] = 'text/plain'; + } + + // Default encoding + if (!isset($this->encoding)) { + $this->encoding = '7bit'; + } + + // Assign stuff to member variables + $this->encoded = array(); + $this->headers = $headers; + $this->body = $body; + } + + /** + * Encodes and returns the email. Also stores + * it in the encoded member variable + * + * @return An associative array containing two elements, + * body and headers. The headers element is itself + * an indexed array. + */ + public function encode() + { + $encoded =& $this->encoded; + + if (!empty($this->subparts)) { + srand((double)microtime()*1000000); + $boundary = '=_' . md5(uniqid(rand()) . microtime()); + $this->headers['Content-Type'] .= ';' . MAIL_MIMEPART_CRLF . "\t" . 'boundary="' . $boundary . '"'; + + // Add body parts to $subparts + for ($i = 0; $i < count($this->subparts); $i++) { + $headers = array(); + $tmp = $this->subparts[$i]->encode(); + foreach ($tmp['headers'] as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $subparts[] = implode(MAIL_MIMEPART_CRLF, $headers) . MAIL_MIMEPART_CRLF . MAIL_MIMEPART_CRLF . $tmp['body']; + } + + $encoded['body'] = '--' . $boundary . MAIL_MIMEPART_CRLF . + implode('--' . $boundary . MAIL_MIMEPART_CRLF, $subparts) . + '--' . $boundary.'--' . MAIL_MIMEPART_CRLF; + } else { + $encoded['body'] = $this->getEncodedData($this->body, $this->encoding) . MAIL_MIMEPART_CRLF; + } + + // Add headers to $encoded + $encoded['headers'] =& $this->headers; + + return $encoded; + } + + /** + * Adds a subpart to current mime part and returns + * a reference to it + * + * @param $body The body of the subpart, if any. + * @param $params The parameters for the subpart, same + * as the $params argument for constructor. + * @return A reference to the part you just added. + */ + public function addSubPart($body, $params) + { + $this->subparts[] = new Mail_MIMEPart($body, $params); + + return $this->subparts[count($this->subparts) - 1]; + } + + /** + * Returns encoded data based upon encoding passed to it + * + * @param $data The data to encode. + * @param $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + */ + private function getEncodedData($data, $encoding) + { + switch ($encoding) { + case '8bit': + case '7bit': + return $data; + break; + + case 'quoted-printable': + return $this->quotedPrintableEncode($data); + break; + + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, MAIL_MIMEPART_CRLF)); + break; + + default: + return $data; + } + } + + /** + * Encodes data to quoted-printable standard. + * + * @param $input The data to encode + * @param $line_max Optional max line length. Should + * not be more than 76 chars + */ + private function quotedPrintableEncode($input , $line_max = 76) + { + $lines = preg_split("/\r?\n/", $input); + $eol = MAIL_MIMEPART_CRLF; + $escape = '='; + $output = ''; + + while(list(, $line) = each($lines)){ + + $linlen = strlen($line); + $newline = ''; + + for ($i = 0; $i < $linlen; $i++) { + $char = substr($line, $i, 1); + $dec = ord($char); + + if (($dec == 32) AND ($i == ($linlen - 1))){ // convert space at eol only + $char = '=20'; + + } elseif($dec == 9) { + ; // Do nothing if a tab. + } elseif(($dec == 61) OR ($dec < 32 ) OR ($dec > 126)) { + $char = $escape . strtoupper(sprintf('%02s', dechex($dec))); + } + + if ((strlen($newline) + strlen($char)) >= $line_max) { // MAIL_MIMEPART_CRLF is not counted + $output .= $newline . $escape . $eol; // soft line break; " =\r\n" is okay + $newline = ''; + } + $newline .= $char; + } // end of for + $output .= $newline . $eol; + } + $output = substr($output, 0, -1 * strlen($eol)); // Don't want last crlf + return $output; + } +} // End of class +?> \ No newline at end of file diff --git a/Rmail/smtp.php b/Rmail/smtp.php new file mode 100644 index 0000000..7f92a06 --- /dev/null +++ b/Rmail/smtp.php @@ -0,0 +1,371 @@ + + * pass - Password for authentication Default: + * timeout - The timeout in seconds for the call Default: 5 + * to fsockopen() + */ + public function __construct($params = array()) + { + + if(!defined('CRLF')) + define('CRLF', "\r\n", TRUE); + + $this->authenticated = FALSE; + $this->timeout = 5; + $this->status = SMTP_STATUS_NOT_CONNECTED; + $this->host = 'localhost'; + $this->port = 25; + $this->helo = 'localhost'; + $this->auth = FALSE; + $this->user = ''; + $this->pass = ''; + $this->errors = array(); + + foreach($params as $key => $value){ + $this->$key = $value; + } + } + + /** + * Connect function. This will, when called + * statically, create a new smtp object, + * call the connect function (ie this function) + * and return it. When not called statically, + * it will connect to the server and send + * the HELO command. + */ + public function connect($params = array()) + { + if (!isset($this->status)) { + $obj = new smtp($params); + if($obj->connect()){ + $obj->status = SMTP_STATUS_CONNECTED; + } + + return $obj; + + } else { + $this->connection = fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout); + if (function_exists('socket_set_timeout')) { + @socket_set_timeout($this->connection, 5, 0); + } + + $greeting = $this->get_data(); + if (is_resource($this->connection)) { + return $this->auth ? $this->ehlo() : $this->helo(); + } else { + $this->errors[] = 'Failed to connect to server: '.$errstr; + return FALSE; + } + } + } + + /** + * Function which handles sending the mail. + * Arguments: + * $params - Optional assoc array of parameters. + * Can contain: + * recipients - Indexed array of recipients + * from - The from address. (used in MAIL FROM:), + * this will be the return path + * headers - Indexed array of headers, one header per array entry + * body - The body of the email + * It can also contain any of the parameters from the connect() + * function + */ + public function send($params = array()) + { + foreach ($params as $key => $value) { + $this->set($key, $value); + } + + if ($this->is_connected()) { + + // Do we auth or not? Note the distinction between the auth variable and auth() function + if ($this->auth AND !$this->authenticated) { + if(!$this->auth()) + return false; + } + + $this->mail($this->from); + + if (is_array($this->recipients)) { + foreach ($this->recipients as $value) { + $this->rcpt($value); + } + } else { + $this->rcpt($this->recipients); + } + + if (!$this->data()) { + return false; + } + + // Transparency + $headers = str_replace(CRLF.'.', CRLF.'..', trim(implode(CRLF, $this->headers))); + $body = str_replace(CRLF.'.', CRLF.'..', $this->body); + $body = substr($body, 0, 1) == '.' ? '.'.$body : $body; + + $this->send_data($headers); + $this->send_data(''); + $this->send_data($body); + $this->send_data('.'); + + $result = (substr(trim($this->get_data()), 0, 3) === '250'); + //$this->rset(); + return $result; + } else { + $this->errors[] = 'Not connected!'; + return FALSE; + } + } + + /** + * Function to implement HELO cmd + */ + private function helo() + { + if(is_resource($this->connection) + AND $this->send_data('HELO '.$this->helo) + AND substr(trim($error = $this->get_data()), 0, 3) === '250' ){ + + return true; + + } else { + $this->errors[] = 'HELO command failed, output: ' . trim(substr(trim($error),3)); + return false; + } + } + + /** + * Function to implement EHLO cmd + */ + private function ehlo() + { + if (is_resource($this->connection) + AND $this->send_data('EHLO '.$this->helo) + AND substr(trim($error = $this->get_data()), 0, 3) === '250' ){ + + return true; + + } else { + $this->errors[] = 'EHLO command failed, output: ' . trim(substr(trim($error),3)); + return false; + } + } + + /** + * Function to implement RSET cmd + */ + private function rset() + { + if (is_resource($this->connection) + AND $this->send_data('RSET') + AND substr(trim($error = $this->get_data()), 0, 3) === '250' ){ + + return true; + + } else { + $this->errors[] = 'RSET command failed, output: ' . trim(substr(trim($error),3)); + return false; + } + } + + /** + * Function to implement QUIT cmd + */ + private function quit() + { + if(is_resource($this->connection) + AND $this->send_data('QUIT') + AND substr(trim($error = $this->get_data()), 0, 3) === '221' ){ + + fclose($this->connection); + $this->status = SMTP_STATUS_NOT_CONNECTED; + return true; + + } else { + $this->errors[] = 'QUIT command failed, output: ' . trim(substr(trim($error),3)); + return false; + } + } + + /** + * Function to implement AUTH cmd + */ + private function auth() + { + if (is_resource($this->connection) + AND $this->send_data('AUTH LOGIN') + AND substr(trim($error = $this->get_data()), 0, 3) === '334' + AND $this->send_data(base64_encode($this->user)) // Send username + AND substr(trim($error = $this->get_data()),0,3) === '334' + AND $this->send_data(base64_encode($this->pass)) // Send password + AND substr(trim($error = $this->get_data()),0,3) === '235' ){ + + $this->authenticated = true; + return true; + + } else { + $this->errors[] = 'AUTH command failed: ' . trim(substr(trim($error),3)); + return false; + } + } + + /** + * Function that handles the MAIL FROM: cmd + */ + private function mail($from) + { + if ($this->is_connected() + AND $this->send_data('MAIL FROM:<'.$from.'>') + AND substr(trim($this->get_data()), 0, 2) === '250' ) { + + return true; + + } else { + return false; + } + } + + /** + * Function that handles the RCPT TO: cmd + */ + private function rcpt($to) + { + if($this->is_connected() + AND $this->send_data('RCPT TO:<'.$to.'>') + AND substr(trim($error = $this->get_data()), 0, 2) === '25' ){ + + return true; + + } else { + $this->errors[] = trim(substr(trim($error), 3)); + return false; + } + } + + /** + * Function that sends the DATA cmd + */ + private function data() + { + if($this->is_connected() + AND $this->send_data('DATA') + AND substr(trim($error = $this->get_data()), 0, 3) === '354' ) { + + return true; + + } else { + $this->errors[] = trim(substr(trim($error), 3)); + return false; + } + } + + /** + * Function to determine if this object + * is connected to the server or not. + */ + private function is_connected() + { + return (is_resource($this->connection) AND ($this->status === SMTP_STATUS_CONNECTED)); + } + + /** + * Function to send a bit of data + */ + private function send_data($data) + { + if(is_resource($this->connection)){ + return fwrite($this->connection, $data.CRLF, strlen($data)+2); + + } else { + return false; + } + } + + /** + * Function to get data. + */ + private function get_data() + { + $return = ''; + $line = ''; + $loops = 0; + + if(is_resource($this->connection)){ + while((strpos($return, CRLF) === FALSE OR substr($line,3,1) !== ' ') AND $loops < 100){ + $line = fgets($this->connection, 512); + $return .= $line; + $loops++; + } + return $return; + + }else + return false; + } + + /** + * Sets a variable + */ + public function set($var, $value) + { + $this->$var = $value; + return true; + } + + /** + * Function to return the errors array + */ + public function getErrors() + { + return $this->errors; + } + + +} // End of class +?> \ No newline at end of file diff --git a/admin/communication.php b/admin/communication.php index 3aab9a7..e7f9d82 100644 --- a/admin/communication.php +++ b/admin/communication.php @@ -26,6 +26,13 @@ require_once("../user.inc.php"); user_auth_required('committee', 'admin'); + function launchQueue() { + if(!file_exists("../data/logs")) { + mkdir("../data/logs"); + } + exec("php -q send_emailqueue.php >>../data/logs/emailqueue.log 2>&1 &"); + } + /* dialog_choose * select: comm_dialog_choose_select(emails_id) * cancel: comm_dialog_choose_cancel() */ @@ -169,6 +176,10 @@ case 'dialog_edit': if(array_key_exists('fundraising_campaigns_id', $_GET)) { $fcid = intval( $_GET['fundraising_campaigns_id']); $type = 'fundraising'; + $q=mysql_query("SELECT * FROM fundraising_campaigns WHERE id='$fcid'"); + $fc=mysql_fetch_object($q); + $name=i18n("%1 communication for %2",array(ucfirst($key),$fc->name)); + $from=$_SESSION['name']." <".$_SESSION['email'].">"; } else { $fcid = 0; $type = (array_key_exists('type',$_GET)) ? $_GET['type'] : 'user'; @@ -316,11 +327,184 @@ case 'dialog_edit': + + + val)."', + '".mysql_real_escape_string($email->name)."', + '".$_SESSION['users_uid']."', + '".mysql_real_escape_string($email->from)."', + '".mysql_real_escape_string($email->subject)."', + '".mysql_real_escape_string($email->body)."', + '".mysql_real_escape_string($email->bodyhtml)."', + '".mysql_real_escape_string($email->type)."', + $fcid, + NOW(), + NULL, + $numtotal, + 0)"); + $emailqueueid=mysql_insert_id(); + echo mysql_error(); + + while($r=mysql_fetch_object($recipq)) { + $u=user_load_by_uid($r->users_uid); + $replacements=array( + "FAIRNAME"=>$config['fairname'], + "SALUTATION"=>$u['salutation'], + "FIRSTNAME"=>$u['firstname'], + "LASTNAME"=>$u['lastname'], + "NAME"=>$u['firstname']." ".$u['lastname'], + "EMAIL"=>$u['email'], + "ORGANIZATION"=>$u['sponsor']['organization'] + ); + if($u['firstname'] && $u['lastname']) + $toname=$u['firstname']." ".$u['lastname']; + else if($u['firstname']) + $toname=$u['firstname']; + else if($u['lastname']) + $toname=$u['lastname']; + + $toemail=$u['email']; + + if($toemail) { + mysql_query("INSERT INTO emailqueue_recipients (emailqueue_id,toemail,toname,replacements,sent) VALUES ( + '$emailqueueid', + '".mysql_real_escape_string($toemail)."', + '".mysql_real_escape_string($toname)."', + '".json_encode($replacements)."', + NULL)"); + echo mysql_error(); + } + mysql_query("UPDATE emails SET lastsent=NOW() WHERE id='$emailid'"); + } + echo "ok"; + launchQueue(); + exit; + } + + + send_header("Communication", array('Committee Main' => 'committee_main.php', 'Administration' => 'admin/index.php'), @@ -328,25 +512,20 @@ case 'dialog_edit': ); echo "
"; - if($_POST['action']=="add") - { - if(!$_POST['val']) - { + if($_POST['action']=="add") { + if(!$_POST['val']) { echo error(i18n("Email Key is required")); $_GET['action']="add"; } - else if(!$_POST['name']) - { + else if(!$_POST['name']) { echo error(i18n("Email Name is required")); $_GET['action']="add"; } - else if(!$_POST['from']) - { + else if(!$_POST['from']) { echo error(i18n("Email From is required")); $_GET['action']="add"; } - else - { + else { mysql_query("INSERT INTO emails (val,name,description,`from`,subject,body,type) VALUES (". "'".mysql_escape_string(stripslashes($_POST['val']))."', ". "'".mysql_escape_string(stripslashes($_POST['name']))."', ". @@ -360,22 +539,18 @@ case 'dialog_edit': } } - if($_POST['action']=="edit") - { - if(!$_POST['name']) - { + if($_POST['action']=="edit") { + if(!$_POST['name']) { echo error(i18n("Email Name is required")); $_GET['action']="edit"; $_GET['edit']=$_POST['edit']; } - else if(!$_POST['from']) - { + else if(!$_POST['from']) { echo error(i18n("Email From is required")); $_GET['action']="edit"; $_GET['edit']=$_POST['edit']; } - else - { + else { mysql_query("UPDATE emails SET ". "name='".mysql_escape_string(stripslashes($_POST['name']))."', ". "description='".mysql_escape_string(stripslashes($_POST['description']))."', ". @@ -386,14 +561,11 @@ case 'dialog_edit': echo mysql_error(); echo happy(i18n("Email successfully saved")); } - } - if($_GET['action']=="delete" && $_GET['delete']) - { + if($_GET['action']=="delete" && $_GET['delete']) { mysql_query("DELETE FROM emails WHERE id='".$_GET['delete']."'"); echo happy("Email successfully deleted"); - } if($_GET['action']=="send" && $_GET['send']) { @@ -492,7 +664,7 @@ case 'dialog_edit': fputs($fp,$_GET['to']."\n"); fclose($fp); - exec("/usr/local/bin/php -q send_communication.php ".$_GET['reallysend']." >/dev/null 2>&1 &"); + exec("php -q send_communication.php ".$_GET['reallysend']." >/dev/null 2>&1 &"); echo "
"; echo happy("Email Communication sending has started!"); diff --git a/admin/communication_send_status.php b/admin/communication_send_status.php index 51220f5..58b4c27 100644 --- a/admin/communication_send_status.php +++ b/admin/communication_send_status.php @@ -26,16 +26,115 @@ require_once("../user.inc.php"); user_auth_required('committee', 'admin'); + if($_GET['action']=="status") { + if($config['emailqueue_lock']) { + echo "

".i18n("Active Send Queues")."

\n"; + $q=mysql_query("SELECT *,UNIX_TIMESTAMP(started) AS ts FROM emailqueue WHERE finished IS NULL ORDER BY started DESC"); + echo ""; + echo ""; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo "\n"; + + while($r=mysql_fetch_object($q)) { + echo ""; + echo " \n"; + echo " \n"; + echo " \n"; + $remaining=$r->numtotal-$r->numsent; + $now=time(); + $duration=$now-$r->ts; + echo " \n"; + echo ""; + echo ""; + echo "\n"; + } + echo "
".i18n("Name")."".i18n("Subject")."".i18n("Started")."".i18n("Progress")."".i18n("Duration")."".i18n("ETA")."
$r->name$r->subject$r->started$r->numsent / $r->numtotal"; + echo format_duration($duration); + echo ""; + if($r->numsent) { + $emailspersecond=$r->numsent/$duration; + $remainingduration=$remaining/$emailspersecond; + echo format_duration($remainingduration); + } + else { + echo "Unknown"; + } + echo "
"; + echo "

\n"; + } + else { + echo notice("No Email Communications are currently being sent out"); + ?> + + ".i18n("Completed Send Queues")."\n"; + echo "\n"; + echo ""; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo "\n"; + while($r=mysql_fetch_object($q)) { + echo ""; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + echo "\n"; + } + echo "
".i18n("Name")."".i18n("Subject")."".i18n("Started")."".i18n("Finished")."".i18n("Total Emails")."
$r->name$r->subject$r->started$r->finished$r->numtotal
\n"; + exit; + } + send_header("Communication Sending Status", array('Committee Main' => 'committee_main.php', 'Administration' => 'admin/index.php', 'Communication' => 'admin/communication.php') ); + ?> + + "; + echo "
"; + echo "
"; + echo "
"; echo "
"; -echo "

".i18n("Communication Sending Status")."

\n"; +echo "

".i18n("Old Send Queue (to be removed / migrated to the above eventually)")."

\n"; echo "
"; - if(file_exists("../data/communication.lock")) { $lines=file("../data/communication.lock"); diff --git a/admin/fundraising_campaigns.php b/admin/fundraising_campaigns.php index 7eac82d..3a45b3e 100644 --- a/admin/fundraising_campaigns.php +++ b/admin/fundraising_campaigns.php @@ -422,7 +422,7 @@ switch($_GET['action']){ echo ""; echo "
"; //we let them always send it again for now... might change this back later, but i think just notifying them of when it was last sent is enough and keeps teh form more consistent - echo ""; + echo "id)\" value=\"".i18n("Send as email")."\" />"; echo "
\n"; if($email->lastsent) { list($date,$time)=split(" ",$email->lastsent); @@ -729,9 +729,10 @@ function opensendmaildialog(fcid,key) { return false; } -function opensendemaildialog() { - alert('not implemented yet'); - +function opensendemaildialog(fcid,emails_id) { + $("#dialog").empty(); + $("#dialog").load("communication.php?action=dialog_send&type=fundraising&fundraising_campaigns_id="+fcid+"&emails_id="+emails_id,null,function() { + }); } diff --git a/admin/send_emailqueue.php b/admin/send_emailqueue.php new file mode 100644 index 0000000..8fb4006 --- /dev/null +++ b/admin/send_emailqueue.php @@ -0,0 +1,98 @@ + + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, version 2. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ +?> +emailqueue_id'"); + $email=mysql_fetch_object($eq); + + $mail=new RMail(); + $mail->setFrom($email->from); + $mail->setSubject($email->subject); + + //FIXME: do the replacements + $blank=array(); + $replacements=(array)json_decode($r->replacements); + print_r($replacements); + + if($email->body) + $mail->setText(communication_replace_vars($email->body,$blank,$replacements)); + else if($email->bodyhtml) { + $mail->setText(strip_tags(communication_replace_vars($email->bodyhtml,$blank,$replacements))); + } + else { + $mail->setText("No message body specified"); + } + + if($email->bodyhtml) + $mail->setHTML(communication_replace_vars($email->bodyhtml,$blank,$replacements)); + + if($r->toname) { + $to="\"$r->toname\" <$r->toemail>"; + } + else { + $to=$r->toemail; + } + echo "$email->id,$r->id,$to\n"; + $result=$mail->send(array($to)); + if($result) { + mysql_query("UPDATE emailqueue_recipients SET sent=NOW() WHERE id='$r->id'"); + $newnumsent=$email->numsent+1; + mysql_query("UPDATE emailqueue SET numsent=$newnumsent WHERE id='$email->id'"); + $rq=mysql_query("SELECT COUNT(*) AS num FROM emailqueue_recipients WHERE sent IS NULL AND emailqueue_id='$email->id'"); + $rr=mysql_fetch_object($rq); + if($rr->num==0) { + mysql_query("UPDATE emailqueue SET finished=NOW() WHERE id='$email->id'"); + } + } + else { + echo "Sending failed!\n"; + break; + } + usleep(rand($sleepmin,$sleepmax)); + } + else + break; + } + mysql_query("UPDATE config SET val='' WHERE var='emailqueue_lock'"); +} +else { + echo "Already locked\n"; +} + +?> diff --git a/common.inc.php b/common.inc.php index e964d04..f1318c2 100644 --- a/common.inc.php +++ b/common.inc.php @@ -873,8 +873,7 @@ function isEmailAddress($str) { return false; } -function communication_replace_vars($text, &$u) -{ +function communication_get_user_replacements(&$u) { global $config; $rep = array('FAIRNAME' => $config['fairname'], 'NAME' => $u['name'], @@ -883,7 +882,21 @@ function communication_replace_vars($text, &$u) 'SALUTATION' => $u['salutation'], 'FIRSTNAME' => $u['firstname'], 'LASTNAME' => $u['lastname'], - ); + 'ORGANIZATION' => $u['sponsor']['organization'], + ); + return $rep; +} + +function communication_replace_vars($text, &$u, $otherrep=array()) { + global $config; + if($u) { + $userrep=communicaton_get_user_replacements($u); + } + else { + $userrep=array(); + } + + $rep=array_merge($userrep,$otherrep); foreach($rep AS $k=>$v) { $text=ereg_replace("\[$k\]",$v,$text); } @@ -1228,5 +1241,32 @@ function colour_to_percent($percent) { } +function format_duration($seconds, $granularity = 2) +{ + $units = array( + '1 year|:count years' => 31536000, + '1 week|:count weeks' => 604800, + '1 day|:count days' => 86400, + '1 hour|:count hours' => 3600, + '1 min|:count min' => 60, + '1 sec|:count sec' => 1); + $output = ''; + // $output.=time()." - ".$timestamp." = ".$seconds; + foreach ($units as $key => $value) { + $key = explode('|', $key); + if ($seconds >= $value) { + $count = floor($seconds / $value); + $output .= ($output ? ' ' : ''); + $output .= ($count == 1) ? $key[0] : str_replace(':count', $count, $key[1]); + $seconds %= $value; + $granularity--; + } + if ($granularity == 0) { + break; + } + } + return $output ? $output : '0 sec'; +} + ?> diff --git a/db/db.code.version.txt b/db/db.code.version.txt index 492dff0..7f1ddd5 100644 --- a/db/db.code.version.txt +++ b/db/db.code.version.txt @@ -1 +1 @@ -152 +153 diff --git a/db/db.update.153.sql b/db/db.update.153.sql new file mode 100644 index 0000000..a003909 --- /dev/null +++ b/db/db.update.153.sql @@ -0,0 +1,27 @@ +CREATE TABLE `emailqueue` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , + `val` VARCHAR( 64 ) NOT NULL , + `name` VARCHAR( 128 ) NOT NULL , + `users_uid` INT NOT NULL , + `from` VARCHAR( 128 ) NOT NULL , + `subject` VARCHAR( 128 ) NOT NULL , + `body` TEXT NOT NULL , + `bodyhtml` TEXT NOT NULL , + `type` ENUM( 'system', 'user', 'fundraising' ) NOT NULL , + `fundraising_campaigns_id` INT NULL , + `started` DATETIME NOT NULL , + `finished` DATETIME NULL , + `numtotal` INT NOT NULL , + `numsent` INT NOT NULL +) ENGINE = MYISAM ; + +CREATE TABLE `emailqueue_recipients` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , + `emailqueue_id` INT UNSIGNED NOT NULL , + `toemail` VARCHAR( 128 ) NOT NULL , + `toname` VARCHAR( 128 ) NOT NULL , + `replacements` TEXT NOT NULL , + `sent` DATETIME NULL +) ENGINE = MYISAM ; + +INSERT INTO `config` (`var`, `val`, `category`, `type`, `type_values`, `ord`, `description`, `year`) VALUES ('emailqueue_lock', '', 'Special', '', '', '', 'The current lock datetime of the email sending queue, or empty if not locked', '0');