forked from team/site
1519 lines
54 KiB
PHP
1519 lines
54 KiB
PHP
<?php
|
|
// CubicleSoft PHP SMTP e-mail functions.
|
|
// (C) 2014 CubicleSoft. All Rights Reserved.
|
|
|
|
// Load dependencies.
|
|
if (!class_exists("UTF8")) require_once str_replace("\\", "/", dirname(__FILE__)) . "/utf8.php";
|
|
if (!class_exists("IPAddr")) require_once str_replace("\\", "/", dirname(__FILE__)) . "/ipaddr.php";
|
|
|
|
class SMTP
|
|
{
|
|
public static $dnsttlcache = array();
|
|
private static $depths = array(), $purifier = false, $html = false;
|
|
|
|
// Reduce dependencies. Duplicates code though.
|
|
private static function FilenameSafe($filename)
|
|
{
|
|
return preg_replace('/[_]+/', "_", preg_replace('/[^A-Za-z0-9_.\-]/', "_", $filename));
|
|
}
|
|
|
|
private static function ReplaceNewlines($replacewith, $data)
|
|
{
|
|
$data = str_replace("\r\n", "\n", $data);
|
|
$data = str_replace("\r", "\n", $data);
|
|
$data = str_replace("\n", $replacewith, $data);
|
|
|
|
return $data;
|
|
}
|
|
|
|
// RFC1341 is a hacky workaround to allow 8-bit over 7-bit transport.
|
|
// Also known as "Quoted Printable".
|
|
public static function ConvertToRFC1341($data, $restrictmore = false)
|
|
{
|
|
$data2 = "";
|
|
|
|
// Ranges are limited so that EBCDIC transport works.
|
|
// Also, PHP's mail() function doesn't deal well with lines that start with '.'.
|
|
// http://us2.php.net/manual/en/function.mail.php
|
|
$y = strlen($data);
|
|
for ($x = 0; $x < $y; $x++)
|
|
{
|
|
$currchr = ord($data[$x]);
|
|
if ($currchr == 9 || $currchr == 32 || ($currchr >= 37 && $currchr <= 45) || ($currchr >= 47 && $currchr <= 60) || $currchr == 62 || $currchr == 63 || ($currchr >= 65 && $currchr <= 90) || $currchr == 95 || ($currchr >= 97 && $currchr <= 122))
|
|
{
|
|
if (!$restrictmore) $data2 .= $data[$x];
|
|
else if (($currchr >= 48 && $currchr <= 57) || ($currchr >= 65 && $currchr <= 90) || ($currchr >= 97 && $currchr <= 122)) $data2 .= sprintf("=%02X", $currchr);
|
|
else $data2 .= $data[$x];
|
|
}
|
|
else if ($currchr == 13 && $x + 1 < $y && ord($data[$x + 1]) == 10)
|
|
{
|
|
$data2 .= "\r\n";
|
|
$x++;
|
|
}
|
|
else
|
|
{
|
|
$data2 .= sprintf("=%02X", $currchr);
|
|
}
|
|
}
|
|
|
|
// Break the string on 75 character boundaries and add '=' character.
|
|
$data2 = explode("\r\n", $data2);
|
|
$result = "";
|
|
foreach ($data2 as $currline)
|
|
{
|
|
$x2 = 0;
|
|
$y2 = strlen($currline);
|
|
while ($x2 + 75 < $y2)
|
|
{
|
|
if ($currline[$x2 + 74] == '=')
|
|
{
|
|
$result .= substr($currline, $x2, 74);
|
|
$x2 += 74;
|
|
}
|
|
else if ($currline[$x2 + 73] == '=')
|
|
{
|
|
$result .= substr($currline, $x2, 73);
|
|
$x2 += 73;
|
|
}
|
|
else
|
|
{
|
|
$result .= substr($currline, $x2, 75);
|
|
$x2 += 75;
|
|
}
|
|
$result .= "=\r\n";
|
|
}
|
|
|
|
if ($x2 < $y2) $result .= substr($currline, $x2, $y2 - $x2);
|
|
$result .= "\r\n";
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function ConvertEmailMessageToRFC1341($data, $restrictmore = false)
|
|
{
|
|
$data = self::ReplaceNewlines("\r\n", $data);
|
|
|
|
return self::ConvertToRFC1341($data, $restrictmore);
|
|
}
|
|
|
|
// RFC1342 is a hacky workaround to encode headers in e-mails.
|
|
public static function ConvertToRFC1342($data, $lang = "UTF-8", $encodeb64 = true)
|
|
{
|
|
$result = "";
|
|
|
|
// An individual RFC1342-compliant string can only be 75 characters long, 6 must be markers,
|
|
// one must be the encoding method, and at least one must be data (adjusted to 4 required
|
|
// spaces to simplify processing).
|
|
if (strlen($lang) > 75 - 6 - 1 - 4) return $result;
|
|
|
|
$lang = strtoupper($lang);
|
|
if ($lang != "ISO-8859-1" && $lang != "US-ASCII") $encodeb64 = true;
|
|
|
|
$maxdatalength = 75 - 6 - strlen($lang) - 1;
|
|
if ($encodeb64)
|
|
{
|
|
$maxdatalength = $maxdatalength * 3 / 4;
|
|
$y = strlen($data);
|
|
if ($lang == "UTF-8")
|
|
{
|
|
$x = 0;
|
|
$pos = 0;
|
|
$size = 0;
|
|
while (UTF8::NextChrPos($data, $y, $pos, $size))
|
|
{
|
|
if ($pos + $size - $x > $maxdatalength)
|
|
{
|
|
if ($x) $result .= " ";
|
|
$result .= "=?" . $lang . "?B?" . base64_encode(substr($data, $x, $pos - $x)) . "?=";
|
|
$x = $pos;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for ($x = 0; $x + $maxdatalength < $y; $x += $maxdatalength)
|
|
{
|
|
if ($x) $result .= " ";
|
|
$result .= "=?" . $lang . "?B?" . base64_encode(substr($data, $x, $maxdatalength)) . "?=";
|
|
}
|
|
}
|
|
|
|
if ($x < $y)
|
|
{
|
|
if ($x) $result .= " ";
|
|
$result .= "=?" . $lang . "?B?" . base64_encode(substr($data, $x, $y - $x)) . "?=";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Quoted printable.
|
|
$maxdatalength = $maxdatalength / 3;
|
|
$y = strlen($data);
|
|
for ($x = 0; $x + $maxdatalength < $y; $x += $maxdatalength)
|
|
{
|
|
if ($x) $result .= " ";
|
|
$result .= "=?" . $lang . "?Q?" . str_replace(" ", "_", self::ConvertToRFC1341(substr($data, $x, $maxdatalength), true)) . "?=";
|
|
}
|
|
if ($x < $y)
|
|
{
|
|
if ($x) $result .= " ";
|
|
$result .= "=?" . $lang . "?Q?" . str_replace(" ", "_", self::ConvertToRFC1341(substr($data, $x, $y - $x), true)) . "?=";
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private static function SMTP_Translate()
|
|
{
|
|
$args = func_get_args();
|
|
if (!count($args)) return "";
|
|
|
|
return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args);
|
|
}
|
|
|
|
// Takes a potentially invalid e-mail address and attempts to make it valid.
|
|
public static function MakeValidEmailAddress($email, $options = array())
|
|
{
|
|
$email = str_replace("\t", " ", $email);
|
|
$email = str_replace("\r", " ", $email);
|
|
$email = str_replace("\n", " ", $email);
|
|
$email = trim($email);
|
|
|
|
// Reverse parse out the initial domain/IP address part of the e-mail address.
|
|
$domain = "";
|
|
$state = "domend";
|
|
$cfwsdepth = 0;
|
|
while ($email != "" && $state != "")
|
|
{
|
|
$prevchr = substr($email, -2, 1);
|
|
$lastchr = substr($email, -1);
|
|
|
|
switch ($state)
|
|
{
|
|
case "domend":
|
|
{
|
|
if ($lastchr == ")")
|
|
{
|
|
$laststate = "domain";
|
|
$state = "cfws";
|
|
}
|
|
else if ($lastchr == "]" || $lastchr == "}")
|
|
{
|
|
$domain .= "]";
|
|
$email = trim(substr($email, 0, -1));
|
|
$state = "ipaddr";
|
|
}
|
|
else
|
|
{
|
|
$state = "domain";
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "cfws":
|
|
{
|
|
if ($prevchr == "\\") $email = trim(substr($email, 0, -2));
|
|
else if ($lastchr == ")")
|
|
{
|
|
$email = trim(substr($email, 0, -1));
|
|
$depth++;
|
|
}
|
|
else if ($lastchr == "(")
|
|
{
|
|
$email = trim(substr($email, 0, -1));
|
|
$depth--;
|
|
if (!$depth && substr($email, -1) != ")") $state = $laststate;
|
|
}
|
|
else $email = trim(substr($email, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "ipaddr":
|
|
{
|
|
if ($lastchr == "[" || $lastchr == "{" || $lastchr == "@")
|
|
{
|
|
$domain .= "[";
|
|
$state = "@";
|
|
|
|
if ($lastchr == "@") break;
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ".") $domain .= ".";
|
|
else if ($lastchr == ";" || $lastchr == ":") $domain .= ":";
|
|
else if (preg_match('/[A-Za-z0-9]/', $lastchr)) $domain .= $lastchr;
|
|
|
|
$email = trim(substr($email, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "domain":
|
|
{
|
|
if ($lastchr == "@")
|
|
{
|
|
$state = "@";
|
|
|
|
break;
|
|
}
|
|
else if ($lastchr == ")")
|
|
{
|
|
$state = "cfws";
|
|
$laststate = "@";
|
|
|
|
break;
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ".") $domain .= ".";
|
|
else if (preg_match('/[A-Za-z0-9-]/', $lastchr)) $domain .= $lastchr;
|
|
|
|
$email = trim(substr($email, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "@":
|
|
{
|
|
if ($lastchr == "@") $state = "";
|
|
|
|
$email = trim(substr($email, 0, -1));
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$domain = strrev($domain);
|
|
$parts = explode(".", $domain);
|
|
foreach ($parts as $num => $part) $parts[$num] = str_replace(" ", "-", trim(str_replace("-", " ", $part)));
|
|
$domain = implode(".", $parts);
|
|
|
|
// Forward parse out the local part of the e-mail address.
|
|
// Remove CFWS (comments, folding whitespace).
|
|
while (substr($email, 0, 1) == "(")
|
|
{
|
|
while ($email != "")
|
|
{
|
|
$currchr = substr($email, 0, 1);
|
|
if ($currchr == "\\") $email = trim(substr($email, 2));
|
|
else if ($currchr == "(")
|
|
{
|
|
$depth++;
|
|
$email = trim(substr($email, 1));
|
|
}
|
|
else if ($currchr == ")")
|
|
{
|
|
$email = trim(substr($email, 1));
|
|
$depth--;
|
|
if (!$depth && substr($email, 0, 1) != "(") break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process quoted/unquoted string.
|
|
$local = "";
|
|
if (substr($email, 0, 1) == "\"")
|
|
{
|
|
$email = substr($email, 1);
|
|
while ($email != "")
|
|
{
|
|
$currchr = substr($email, 0, 1);
|
|
$nextchr = substr($email, 1, 1);
|
|
|
|
if ($currchr == "\\")
|
|
{
|
|
if ($nextchr == "\\" || $nextchr == "\"")
|
|
{
|
|
$local .= substr($email, 0, 2);
|
|
$email = substr($email, 2);
|
|
}
|
|
else if (ord($nextchr) >= 33 && ord($nextchr) <= 126)
|
|
{
|
|
$local .= substr($email, 1, 1);
|
|
$email = substr($email, 2);
|
|
}
|
|
}
|
|
else if ($currchr == "\"") break;
|
|
else if (ord($currchr) >= 33 && ord($nextchr) <= 126)
|
|
{
|
|
$local .= substr($email, 0, 1);
|
|
$email = substr($email, 1);
|
|
}
|
|
else $email = substr($email, 1);
|
|
}
|
|
|
|
if (substr($local, -1) != "\"") $local .= "\"";
|
|
}
|
|
else
|
|
{
|
|
while ($email != "")
|
|
{
|
|
$currchr = substr($email, 0, 1);
|
|
|
|
if (preg_match("/[A-Za-z0-9]/", $currchr) || $currchr == "!" || $currchr == "#" || $currchr == "\$" || $currchr == "%" || $currchr == "&" || $currchr == "'" || $currchr == "*" || $currchr == "+" || $currchr == "-" || $currchr == "/" || $currchr == "=" || $currchr == "?" || $currchr == "^" || $currchr == "_" || $currchr == "`" || $currchr == "{" || $currchr == "|" || $currchr == "}" || $currchr == "~" || $currchr == ".")
|
|
{
|
|
$local .= $currchr;
|
|
$email = substr($email, 1);
|
|
}
|
|
else break;
|
|
}
|
|
|
|
$local = preg_replace('/[.]+/', ".", $local);
|
|
if (substr($local, 0, 1) == ".") $local = substr($local, 1);
|
|
if (substr($local, -1) == ".") $local = substr($local, 0, -1);
|
|
}
|
|
while (substr($local, -2) == "\\\"") $local = substr($local, 0, -2) . "\"";
|
|
if ($local == "\"" || $local == "\"\"") $local = "";
|
|
|
|
// Analyze the domain/IP part and fix any issues.
|
|
$domain = preg_replace('/[.]+/', ".", $domain);
|
|
if (substr($domain, -1) == "]")
|
|
{
|
|
if (substr($domain, 0, 1) != "[") $domain = "[" . $domain;
|
|
|
|
// Process the IP address.
|
|
if (strtolower(substr($domain, 0, 6)) == "[ipv6:") $ipaddr = IPAddr::NormalizeIP(substr($domain, 6, -1));
|
|
else $ipaddr = IPAddr::NormalizeIP(substr($domain, 1, -1));
|
|
|
|
if ($ipaddr["ipv4"] != "") $domain = "[" . $ipaddr["ipv4"] . "]";
|
|
else $domain = "[IPv6:" . $ipaddr["ipv6"] . "]";
|
|
}
|
|
else
|
|
{
|
|
// Process the domain.
|
|
if (substr($domain, 0, 1) == ".") $domain = substr($domain, 1);
|
|
if (substr($domain, -1) == ".") $domain = substr($domain, 0, -1);
|
|
$domain = explode(".", $domain);
|
|
foreach ($domain as $num => $part)
|
|
{
|
|
if (substr($part, 0, 1) == "-") $part = substr($part, 1);
|
|
if (substr($part, -1) == "-") $part = substr($part, 0, -1);
|
|
if (strlen($part) > 63) $part = substr($part, 0, 63);
|
|
|
|
$domain[$num] = $part;
|
|
}
|
|
|
|
$domain = implode(".", $domain);
|
|
}
|
|
|
|
// Validate the final lengths.
|
|
$y = strlen($local);
|
|
$y2 = strlen($domain);
|
|
$email = $local . "@" . $domain;
|
|
if (!$y) return array("success" => false, "error" => self::SMTP_Translate("Missing local part of e-mail address."), "errorcode" => "missing_local_part", "info" => $email);
|
|
if (!$y2) return array("success" => false, "error" => self::SMTP_Translate("Missing domain part of e-mail address."), "errorcode" => "missing_domain_part", "info" => $email);
|
|
if ($y > 64 || $y2 > 253 || $y + $y2 + 1 > 253) return array("success" => false, "error" => self::SMTP_Translate("E-mail address is too long."), "errorcode" => "email_too_long", "info" => $email);
|
|
|
|
// Process results.
|
|
if (substr($domain, 0, 1) == "[" && substr($domain, -1) == "]") $result = array("success" => true, "email" => $email, "lookup" => false, "type" => "IP");
|
|
else if (isset($options["usedns"]) && $options["usedns"] === false) $result = array("success" => true, "email" => $email, "lookup" => false, "type" => "Domain");
|
|
else if ((!isset($options["usednsttlcache"]) || $options["usednsttlcache"] === true) && isset(self::$dnsttlcache[$domain]) && self::$dnsttlcache[$domain] >= time()) $result = array("success" => true, "email" => $email, "lookup" => false, "type" => "CachedDNS");
|
|
else
|
|
{
|
|
// Check for a mail server based on a DNS lookup.
|
|
$result = self::GetDNSRecord($domain, array("MX", "A"), (isset($options["nameservers"]) ? $options["nameservers"] : array("8.8.8.8", "8.8.4.4")), (!isset($options["usednsttlcache"]) || $options["usednsttlcache"] === true));
|
|
if ($result["success"]) $result = array("success" => true, "email" => $email, "lookup" => true, "type" => $result["type"], "records" => $result["records"]);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function UpdateDNSTTLCache()
|
|
{
|
|
$ts = time();
|
|
foreach (self::$dnsttlcache as $domain => $ts2)
|
|
{
|
|
if ($ts2 > $ts) unset(self::$dnsttlcache[$domain]);
|
|
}
|
|
}
|
|
|
|
public static function GetDNSRecord($domain, $types = array("MX", "A"), $nameservers = array("8.8.8.8", "8.8.4.4"), $cache = true)
|
|
{
|
|
// Check for a mail server based on a DNS lookup.
|
|
if (!class_exists("Net_DNS2_Resolver")) require_once str_replace("\\", "/", dirname(__FILE__)) . "/Net/DNS2.php";
|
|
|
|
$resolver = new Net_DNS2_Resolver(array("nameservers" => $nameservers));
|
|
try
|
|
{
|
|
foreach ($types as $type)
|
|
{
|
|
$response = $resolver->query($domain, $type);
|
|
if ($response && count($response->answer))
|
|
{
|
|
if ($cache)
|
|
{
|
|
$minttl = -1;
|
|
foreach ($response->answer as $answer)
|
|
{
|
|
if ($minttl < 0 || ($answer->ttl > 0 && $answer->ttl < $minttl)) $minttl = $answer->ttl;
|
|
}
|
|
|
|
self::$dnsttlcache[$domain] = time() + $minttl;
|
|
}
|
|
|
|
return array("success" => true, "type" => $type, "records" => $response);
|
|
}
|
|
}
|
|
|
|
return array("success" => false, "error" => self::SMTP_Translate("Invalid domain name or missing DNS record."), "errorcode" => "invalid_domain_or_missing_record", "info" => $domain);
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
return array("success" => false, "error" => self::SMTP_Translate("Invalid domain name. Internal exception occurred."), "errorcode" => "dns_library_exception", "info" => self::SMTP_Translate("%s (%s).", $e->getMessage(), $domain));
|
|
}
|
|
}
|
|
|
|
public static function EmailAddressesToNamesAndEmail(&$destnames, &$destaddrs, $emailaddrs, $removenames = false, $options = array())
|
|
{
|
|
$destnames = array();
|
|
$destaddrs = array();
|
|
|
|
$data = str_replace("\t", " ", $emailaddrs);
|
|
$data = str_replace("\r", " ", $data);
|
|
$data = str_replace("\n", " ", $data);
|
|
$data = trim($data);
|
|
|
|
// Parse e-mail addresses out of the string with a state engine.
|
|
// Parsed in reverse because that is easier than trying to figure out if each address
|
|
// starts with a name OR a quoted string for the local part of the e-mail address.
|
|
// The e-mail address parsing in this state engine is intentionally incomplete.
|
|
// The goal is to identify '"name" <emailaddr>, name <emailaddr>, emailaddr' variations.
|
|
$found = false;
|
|
while ($data != "")
|
|
{
|
|
$name = "";
|
|
$email = "";
|
|
$state = "addrend";
|
|
$cfwsdepth = 0;
|
|
$inbracket = false;
|
|
|
|
while ($data != "" && $state != "")
|
|
{
|
|
$prevchr = substr($data, -2, 1);
|
|
$lastchr = substr($data, -1);
|
|
|
|
switch ($state)
|
|
{
|
|
case "addrend":
|
|
{
|
|
if ($lastchr == ">")
|
|
{
|
|
$data = trim(substr($data, 0, -1));
|
|
$inbracket = true;
|
|
$state = "domend";
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ";")
|
|
{
|
|
$data = trim(substr($data, 0, -1));
|
|
}
|
|
else $state = "domend";
|
|
|
|
break;
|
|
}
|
|
case "domend":
|
|
{
|
|
if ($lastchr == ")")
|
|
{
|
|
$laststate = "domain";
|
|
$state = "cfws";
|
|
}
|
|
else if ($lastchr == "]" || $lastchr == "}")
|
|
{
|
|
$email .= "]";
|
|
$data = trim(substr($data, 0, -1));
|
|
$state = "ipaddr";
|
|
}
|
|
else
|
|
{
|
|
$state = "domain";
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "cfws":
|
|
{
|
|
if ($prevchr == "\\") $data = trim(substr($data, 0, -2));
|
|
else if ($lastchr == ")")
|
|
{
|
|
$data = trim(substr($data, 0, -1));
|
|
$depth++;
|
|
}
|
|
else if ($lastchr == "(")
|
|
{
|
|
$data = trim(substr($data, 0, -1));
|
|
$depth--;
|
|
if (!$depth && substr($data, -1) != ")") $state = $laststate;
|
|
}
|
|
else $data = trim(substr($data, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "ipaddr":
|
|
{
|
|
if ($lastchr == "[" || $lastchr == "{" || $lastchr == "@")
|
|
{
|
|
$email .= "[";
|
|
$state = "@";
|
|
|
|
if ($lastchr == "@") break;
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ".") $email .= ".";
|
|
else if ($lastchr == ";" || $lastchr == ":") $email .= ":";
|
|
else if (preg_match('/[A-Za-z0-9]/', $lastchr)) $email .= $lastchr;
|
|
|
|
$data = trim(substr($data, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "domain":
|
|
{
|
|
if ($lastchr == "@")
|
|
{
|
|
$state = "@";
|
|
|
|
break;
|
|
}
|
|
else if ($lastchr == ")")
|
|
{
|
|
$state = "cfws";
|
|
$laststate = "@";
|
|
|
|
break;
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ".") $email .= ".";
|
|
else if (preg_match('/[A-Za-z0-9-]/', $lastchr)) $email .= $lastchr;
|
|
|
|
$data = trim(substr($data, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "@":
|
|
{
|
|
if ($lastchr == "@")
|
|
{
|
|
$email .= "@";
|
|
$state = "localend";
|
|
}
|
|
|
|
$data = trim(substr($data, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "localend":
|
|
{
|
|
if ($lastchr == ")")
|
|
{
|
|
$state = "cfws";
|
|
$laststate = "localend";
|
|
}
|
|
else if ($lastchr == "\"")
|
|
{
|
|
$email .= "\"";
|
|
$data = substr($data, 0, -1);
|
|
$state = "quotedlocal";
|
|
}
|
|
else $state = "local";
|
|
|
|
break;
|
|
}
|
|
case "quotedlocal":
|
|
{
|
|
if ($prevchr == "\\")
|
|
{
|
|
$email .= $lastchar . $prevchr;
|
|
$data = substr($data, 0, -2);
|
|
}
|
|
else if ($lastchr == "\"")
|
|
{
|
|
$email .= $lastchar;
|
|
$data = trim(substr($data, 0, -1));
|
|
$state = "localstart";
|
|
}
|
|
else
|
|
{
|
|
$email .= $lastchar;
|
|
$data = substr($data, 0, -1);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "local":
|
|
{
|
|
if (preg_match("/[A-Za-z0-9]/", $lastchr) || $lastchr == "!" || $lastchr == "#" || $lastchr == "\$" || $lastchr == "%" || $lastchr == "&" || $lastchr == "'" || $lastchr == "*" || $lastchr == "+" || $lastchr == "-" || $lastchr == "/" || $lastchr == "=" || $lastchr == "?" || $lastchr == "^" || $lastchr == "_" || $lastchr == "`" || $lastchr == "{" || $lastchr == "|" || $lastchr == "}" || $lastchr == "~" || $lastchr == ".")
|
|
{
|
|
$email .= $lastchr;
|
|
$data = substr($data, 0, -1);
|
|
}
|
|
else if ($lastchr == ")")
|
|
{
|
|
$state = "cfws";
|
|
$laststate = "localstart";
|
|
}
|
|
else if ($inbracket)
|
|
{
|
|
if ($lastchr == "<") $state = "localstart";
|
|
else $data = substr($data, 0, -1);
|
|
}
|
|
else if ($lastchr == " " || $lastchr == "," || $lastchr == ";") $state = "localstart";
|
|
else $data = substr($data, 0, -1);
|
|
|
|
break;
|
|
}
|
|
case "localstart":
|
|
{
|
|
if ($inbracket)
|
|
{
|
|
if ($lastchr == "<") $state = "nameend";
|
|
|
|
$data = trim(substr($data, 0, -1));
|
|
}
|
|
else if ($lastchr == "," || $lastchr == ";") $state = "";
|
|
else $data = trim(substr($data, 0, -1));
|
|
|
|
break;
|
|
}
|
|
case "nameend":
|
|
{
|
|
if ($lastchr == "\"")
|
|
{
|
|
$data = substr($data, 0, -1);
|
|
$state = "quotedname";
|
|
}
|
|
else $state = "name";
|
|
|
|
break;
|
|
}
|
|
case "quotedname":
|
|
{
|
|
if ($prevchr == "\\")
|
|
{
|
|
$name .= $lastchar . $prevchr;
|
|
$data = substr($data, 0, -2);
|
|
}
|
|
else if ($lastchr == "\"")
|
|
{
|
|
$data = trim(substr($data, 0, -1));
|
|
$state = "";
|
|
}
|
|
else
|
|
{
|
|
$name .= $lastchr;
|
|
$data = substr($data, 0, -1);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "name":
|
|
{
|
|
if ($lastchr == "," || $lastchr == ";") $state = "";
|
|
else
|
|
{
|
|
$name .= $lastchr;
|
|
$data = substr($data, 0, -1);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$email = self::MakeValidEmailAddress(strrev($email), $options);
|
|
if ($email["success"])
|
|
{
|
|
if ($removenames) $name = "";
|
|
$name = trim(strrev($name));
|
|
if (substr($name, 0, 1) == "\"") $name = trim(substr($name, 1));
|
|
$name = str_replace("\\\\", "\\", $name);
|
|
$name = str_replace("\\\"", "\"", $name);
|
|
|
|
$destnames[] = $name;
|
|
$destaddrs[] = $email["email"];
|
|
|
|
$found = true;
|
|
}
|
|
|
|
$data = trim($data);
|
|
}
|
|
|
|
$destnames = array_reverse($destnames);
|
|
$destaddrs = array_reverse($destaddrs);
|
|
|
|
return $found;
|
|
}
|
|
|
|
// Takes in a comma-separated list of e-mail addresses and returns appropriate e-mail headers.
|
|
public static function EmailAddressesToEmailHeaders($emailaddrs, $headername, $multiple = true, $removenames = false, $options = array())
|
|
{
|
|
$result = "";
|
|
|
|
$tempnames = array();
|
|
$tempaddrs = array();
|
|
self::EmailAddressesToNamesAndEmail($tempnames, $tempaddrs, $emailaddrs, $removenames, $options);
|
|
|
|
$y = count($tempnames);
|
|
for ($x = 0; $x < $y && ($multiple || $result == ""); $x++)
|
|
{
|
|
$name = $tempnames[$x];
|
|
$emailaddr = $tempaddrs[$x];
|
|
|
|
if ($name != "" && !UTF8::IsASCII($name)) $name = self::ConvertToRFC1342($name) . " ";
|
|
else if ($name != "") $name = '"' . $name . '" ';
|
|
if ($result != "") $result .= ",\r\n ";
|
|
if ($name != "") $result .= $name . '<' . $emailaddr . '>';
|
|
else $result .= $emailaddr;
|
|
}
|
|
|
|
if ($result != "" && $headername != "") $result = $headername . ": " . $result . "\r\n";
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function GetUserAgent($type)
|
|
{
|
|
if ($type == "Thunderbird") return "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Thunderbird/24.0\r\n";
|
|
else if ($type == "Thunderbird2") return "X-Mailer: Thunderbird 2.0.0.16 (Windows/20080708)\r\n";
|
|
else if ($type == "OutlookExpress") return "X-Mailer: Microsoft Outlook Express 6.00.2900.3198\r\nX-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.3198\r\n";
|
|
else if ($type == "Exchange") return "X-Mailer: Produced By Microsoft Exchange V6.0.6619.12\r\n";
|
|
else if ($type == "OfficeOutlook") return "X-Mailer: Microsoft Office Outlook 12.0\r\n";
|
|
|
|
return "";
|
|
}
|
|
|
|
public static function GetTimeLeft($start, $limit)
|
|
{
|
|
if ($limit === false) return false;
|
|
|
|
$difftime = microtime(true) - $start;
|
|
if ($difftime >= $limit) return 0;
|
|
|
|
return $limit - $difftime;
|
|
}
|
|
|
|
private static function ProcessRateLimit($size, $start, $limit, $async)
|
|
{
|
|
$difftime = microtime(true) - $start;
|
|
if ($difftime > 0.0)
|
|
{
|
|
if ($size / $difftime > $limit)
|
|
{
|
|
// Sleeping for some amount of time will equalize the rate.
|
|
// So, solve this for $x: $size / ($x + $difftime) = $limit
|
|
$amount = ($size - ($limit * $difftime)) / $limit;
|
|
|
|
if ($async) return microtime(true) + $amount;
|
|
else usleep($amount);
|
|
}
|
|
}
|
|
|
|
return -1.0;
|
|
}
|
|
|
|
private static function StreamTimedOut($fp)
|
|
{
|
|
if (!function_exists("stream_get_meta_data")) return false;
|
|
|
|
$info = stream_get_meta_data($fp);
|
|
|
|
return $info["timed_out"];
|
|
}
|
|
|
|
// Reads one or more lines in.
|
|
private static function ProcessState__ReadLine(&$state)
|
|
{
|
|
while (strpos($state["data"], "\n") === false)
|
|
{
|
|
$data2 = @fgets($state["fp"], 116000);
|
|
if ($data2 === false) return array("success" => false, "error" => self::SMTP_Translate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error");
|
|
if (strpos($data2, "\n") === false)
|
|
{
|
|
if (feof($state["fp"])) return array("success" => false, "error" => self::SMTP_Translate("Remote peer disconnected."), "errorcode" => "peer_disconnected");
|
|
if (self::StreamTimedOut($state["fp"])) return array("success" => false, "error" => self::SMTP_Translate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded");
|
|
|
|
if ($state["async"] && $data2 === "") return array("success" => false, "error" => self::SMTP_Translate("Non-blocking read returned no data."), "errorcode" => "no_data");
|
|
}
|
|
if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return array("success" => false, "error" => self::SMTP_Translate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");
|
|
|
|
$state["result"]["rawrecvsize"] += strlen($data2);
|
|
$state["data"] .= $data2;
|
|
|
|
if (isset($state["options"]["recvratelimit"])) $state["waituntil"] = self::ProcessRateLimit($state["rawsize"], $state["recvstart"], $state["options"]["recvratelimit"], $state["async"]);
|
|
|
|
if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"])) call_user_func_array($state["options"]["debug_callback"], array("rawrecv", $data2, &$state["options"]["debug_callback_opts"]));
|
|
else if ($state["debug"]) $state["result"]["rawrecv"] .= $data2;
|
|
}
|
|
|
|
return array("success" => true);
|
|
}
|
|
|
|
// Writes data out.
|
|
private static function ProcessState__WriteData(&$state)
|
|
{
|
|
if ($state["data"] !== "")
|
|
{
|
|
$result = @fwrite($state["fp"], $state["data"]);
|
|
if ($result === false || feof($state["fp"])) return array("success" => false, "error" => self::SMTP_Translate("A fwrite() failure occurred. Most likely cause: Connection failure."), "errorcode" => "fwrite_failed");
|
|
if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return array("success" => false, "error" => self::SMTP_Translate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");
|
|
|
|
$data2 = substr($state["data"], 0, $result);
|
|
$state["data"] = (string)substr($state["data"], $result);
|
|
|
|
$state["result"]["rawsendsize"] += $result;
|
|
|
|
if (isset($state["options"]["sendratelimit"]))
|
|
{
|
|
$state["waituntil"] = self::ProcessRateLimit($state["result"]["rawsendsize"], $state["result"]["connected"], $state["options"]["sendratelimit"], $state["async"]);
|
|
if (microtime(true) < $state["waituntil"]) return array("success" => false, "error" => self::SMTP_Translate("Rate limit for non-blocking connection has not been reached."), "errorcode" => "no_data");
|
|
}
|
|
|
|
if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"])) call_user_func_array($state["options"]["debug_callback"], array("rawsend", $data2, &$state["options"]["debug_callback_opts"]));
|
|
else if ($state["debug"]) $state["result"]["rawsend"] .= $data2;
|
|
}
|
|
|
|
return array("success" => true);
|
|
}
|
|
|
|
public static function ForceClose(&$state)
|
|
{
|
|
if ($state["fp"] !== false)
|
|
{
|
|
@fclose($state["fp"]);
|
|
$state["fp"] = false;
|
|
}
|
|
|
|
if (isset($state["currentfile"]) && $state["currentfile"] !== false)
|
|
{
|
|
if ($state["currentfile"]["fp"] !== false) @fclose($state["currentfile"]["fp"]);
|
|
$state["currentfile"] = false;
|
|
}
|
|
}
|
|
|
|
private static function CleanupErrorState(&$state, $result)
|
|
{
|
|
if (!$result["success"] && $result["errorcode"] !== "no_data")
|
|
{
|
|
self::ForceClose($state);
|
|
|
|
$state["error"] = $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private static function InitSMTPRequest(&$state, $command, $expectedcode, $nextstate, $expectederror)
|
|
{
|
|
$state["data"] = $command . "\r\n";
|
|
$state["state"] = "send_request";
|
|
$state["expectedcode"] = $expectedcode;
|
|
$state["nextstate"] = $nextstate;
|
|
$state["expectederror"] = $expectederror;
|
|
}
|
|
|
|
public static function ProcessState(&$state)
|
|
{
|
|
if (isset($state["error"])) return $state["error"];
|
|
|
|
if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return self::CleanupErrorState($state, array("success" => false, "error" => self::SMTP_Translate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"));
|
|
if (microtime(true) < $state["waituntil"]) return array("success" => false, "error" => self::SMTP_Translate("Rate limit for non-blocking connection has not been reached."), "errorcode" => "no_data");
|
|
|
|
while ($state["state"] !== "done")
|
|
{
|
|
switch ($state["state"])
|
|
{
|
|
case "connecting":
|
|
{
|
|
if (function_exists("stream_select") && $state["async"])
|
|
{
|
|
$readfp = NULL;
|
|
$writefp = array($state["fp"]);
|
|
$exceptfp = NULL;
|
|
$result = @stream_select($readfp, $writefp, $exceptfp, 0);
|
|
if ($result === false) return self::CleanupErrorState($state, array("success" => false, "error" => self::SMTP_Translate("A stream_select() failure occurred. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"));
|
|
|
|
if (!count($writefp)) return array("success" => false, "error" => self::SMTP_Translate("Connection not established yet."), "errorcode" => "no_data");
|
|
}
|
|
|
|
// Handle peer certificate retrieval.
|
|
if (function_exists("stream_context_get_options"))
|
|
{
|
|
$contextopts = stream_context_get_options($state["fp"]);
|
|
if ($state["secure"] && isset($state["options"]["sslopts"]) && is_array($state["options"]["sslopts"]) && isset($contextopts["ssl"]["peer_certificate"]))
|
|
{
|
|
if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"])) call_user_func_array($state["options"]["debug_callback"], array("peercert", @openssl_x509_parse($contextopts["ssl"]["peer_certificate"]), &$state["options"]["debug_callback_opts"]));
|
|
}
|
|
}
|
|
|
|
// Deal with failed connections that hang applications.
|
|
if (isset($state["options"]["streamtimeout"]) && $state["options"]["streamtimeout"] !== false && function_exists("stream_set_timeout")) @stream_set_timeout($state["fp"], $state["options"]["streamtimeout"]);
|
|
|
|
$state["result"]["connected"] = microtime(true);
|
|
|
|
$state["data"] = "";
|
|
$state["code"] = 0;
|
|
$state["expectedcode"] = 220;
|
|
$state["expectederror"] = self::SMTP_Translate("Expected a 220 response from the SMTP server upon connecting.");
|
|
$state["response"] = "";
|
|
$state["state"] = "get_response";
|
|
$state["nextstate"] = "helo_ehlo";
|
|
|
|
break;
|
|
}
|
|
case "send_request":
|
|
{
|
|
// Send the request to the server.
|
|
$result = self::ProcessState__WriteData($state);
|
|
if (!$result["success"]) return self::CleanupErrorState($state, $result);
|
|
|
|
$state["code"] = 0;
|
|
$state["response"] = "";
|
|
|
|
// Handle QUIT differently.
|
|
$state["state"] = ($state["nextstate"] === "done" ? "done" : "get_response");
|
|
|
|
break;
|
|
}
|
|
case "get_response":
|
|
{
|
|
$result = self::ProcessState__ReadLine($state);
|
|
if (!$result["success"]) return self::CleanupErrorState($state, $result);
|
|
|
|
$currline = $state["data"];
|
|
$state["data"] = "";
|
|
if (strlen($currline) >= 4)
|
|
{
|
|
$state["response"] .= substr($currline, 4);
|
|
$state["code"] = (int)substr($currline, 0, 3);
|
|
if (substr($currline, 3, 1) == " ")
|
|
{
|
|
if ($state["expectedcode"] > 0 && $state["code"] !== $state["expectedcode"]) return self::CleanupErrorState($state, array("success" => false, "error" => $state["expectederror"], "errorcode" => "invalid_response", "info" => $state["code"] . " " . $state["response"]));
|
|
|
|
$state["response"] = self::ReplaceNewlines("\r\n", $state["response"]);
|
|
|
|
$state["state"] = $state["nextstate"];
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "helo_ehlo":
|
|
{
|
|
// Send EHLO or HELO depending on server support.
|
|
$hostname = (isset($state["options"]["hostname"]) ? $state["options"]["hostname"] : "[" . trim(isset($_SERVER["SERVER_ADDR"]) && $_SERVER["SERVER_ADDR"] != "127.0.0.1" ? $_SERVER["SERVER_ADDR"] : "192.168.0.101") . "]");
|
|
$state["size_supported"] = 0;
|
|
if (strpos($state["response"], " ESMTP") !== false)
|
|
{
|
|
self::InitSMTPRequest($state, "EHLO " . $hostname, 250, "esmtp_extensions", self::SMTP_Translate("Expected a 250 response from the SMTP server upon EHLO."));
|
|
}
|
|
else
|
|
{
|
|
self::InitSMTPRequest($state, "HELO " . $hostname, 250, "mail_from", self::SMTP_Translate("Expected a 250 response from the SMTP server upon HELO."));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "esmtp_extensions":
|
|
{
|
|
// Process supported ESMTP extensions.
|
|
$auth = "";
|
|
$smtpdata = explode("\r\n", $state["response"]);
|
|
$y = count($smtpdata);
|
|
for ($x = 1; $x < $y; $x++)
|
|
{
|
|
if (strtoupper(substr($smtpdata[$x], 0, 4)) == "AUTH" && ($smtpdata[$x][4] == ' ' || $smtpdata[$x][4] == '=')) $auth = strtoupper(substr($smtpdata[$x], 5));
|
|
if (strtoupper(substr($smtpdata[$x], 0, 4)) == "SIZE" && ($smtpdata[$x][4] == ' ' || $smtpdata[$x][4] == '=')) $state["size_supported"] = (int)substr($smtpdata[$x], 5);
|
|
}
|
|
|
|
$state["state"] = "mail_from";
|
|
|
|
// Process login (if any and supported).
|
|
if (strpos($auth, "LOGIN") !== false)
|
|
{
|
|
$state["username"] = (isset($state["options"]["username"]) ? (string)$state["options"]["username"] : "");
|
|
$state["password"] = (isset($state["options"]["password"]) ? (string)$state["options"]["password"] : "");
|
|
if ($state["username"] !== "" || $state["password"] !== "")
|
|
{
|
|
self::InitSMTPRequest($state, "AUTH LOGIN", 334, "auth_login_username", self::SMTP_Translate("Expected a 334 response from the SMTP server upon AUTH LOGIN."));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "auth_login_username":
|
|
{
|
|
self::InitSMTPRequest($state, base64_encode($state["username"]), 334, "auth_login_password", self::SMTP_Translate("Expected a 334 response from the SMTP server upon AUTH LOGIN username."));
|
|
|
|
break;
|
|
}
|
|
case "auth_login_password":
|
|
{
|
|
self::InitSMTPRequest($state, base64_encode($state["password"]), 235, "mail_from", self::SMTP_Translate("Expected a 235 response from the SMTP server upon AUTH LOGIN password."));
|
|
|
|
break;
|
|
}
|
|
case "mail_from":
|
|
{
|
|
self::InitSMTPRequest($state, "MAIL FROM:<" . $state["fromaddrs"][0] . ">" . ($state["size_supported"] ? " SIZE=" . strlen($state["message"]) : ""), 250, "rcpt_to", self::SMTP_Translate("Expected a 250 response from the SMTP server upon MAIL FROM."));
|
|
|
|
break;
|
|
}
|
|
case "rcpt_to":
|
|
{
|
|
$addr = array_shift($state["toaddrs"]);
|
|
self::InitSMTPRequest($state, "RCPT TO:<" . $addr . ">", 250, (count($state["toaddrs"]) ? "rcpt_to" : "data"), self::SMTP_Translate("Expected a 250 response from the SMTP server upon RCPT TO."));
|
|
|
|
break;
|
|
}
|
|
case "data":
|
|
{
|
|
self::InitSMTPRequest($state, "DATA", 354, "send_message", self::SMTP_Translate("Expected a 354 response from the SMTP server upon DATA."));
|
|
|
|
break;
|
|
}
|
|
case "send_message":
|
|
{
|
|
self::InitSMTPRequest($state, $state["message"] . "\r\n.", 250, "quit", self::SMTP_Translate("Expected a 250 response from the SMTP server upon sending the e-mail."));
|
|
|
|
break;
|
|
}
|
|
case "quit":
|
|
{
|
|
self::InitSMTPRequest($state, "QUIT", 0, "done", "");
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$state["result"]["endts"] = microtime(true);
|
|
|
|
fclose($state["fp"]);
|
|
|
|
return $state["result"];
|
|
}
|
|
|
|
private static function SMTP_RandomHexString($length)
|
|
{
|
|
$lookup = "0123456789ABCDEF";
|
|
$result = "";
|
|
|
|
while ($length)
|
|
{
|
|
$result .= $lookup[mt_rand(0, 15)];
|
|
|
|
$length--;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private static function ProcessSSLOptions(&$options, $key, $host)
|
|
{
|
|
if (isset($options[$key]["auto_cainfo"]))
|
|
{
|
|
unset($options[$key]["auto_cainfo"]);
|
|
|
|
$cainfo = ini_get("curl.cainfo");
|
|
if ($cainfo !== false && strlen($cainfo) > 0) $options[$key]["cafile"] = $cainfo;
|
|
else if (file_exists(str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem")) $options[$key]["cafile"] = str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem";
|
|
}
|
|
|
|
if (isset($options[$key]["auto_cn_match"]))
|
|
{
|
|
unset($options[$key]["auto_cn_match"]);
|
|
|
|
$options[$key]["CN_match"] = $host;
|
|
}
|
|
|
|
if (isset($options[$key]["auto_sni"]))
|
|
{
|
|
unset($options[$key]["auto_sni"]);
|
|
|
|
$options[$key]["SNI_enabled"] = true;
|
|
$options[$key]["SNI_server_name"] = $host;
|
|
}
|
|
}
|
|
|
|
// Sends an e-mail by directly connecting to a SMTP server using PHP sockets. Much more powerful than calling mail().
|
|
public static function SendSMTPEmail($toaddr, $fromaddr, $message, $options = array())
|
|
{
|
|
$startts = microtime(true);
|
|
$timeout = (isset($options["timeout"]) ? $options["timeout"] : false);
|
|
|
|
if (!function_exists("stream_socket_client") && !function_exists("fsockopen")) return array("success" => false, "error" => self::SMTP_Translate("The functions 'stream_socket_client' and 'fsockopen' do not exist."), "errorcode" => "function_check");
|
|
|
|
$temptonames = array();
|
|
$temptoaddrs = array();
|
|
$tempfromnames = array();
|
|
$tempfromaddrs = array();
|
|
if (!self::EmailAddressesToNamesAndEmail($temptonames, $temptoaddrs, $toaddr, true, $options)) return array("success" => false, "error" => self::SMTP_Translate("Invalid 'To' e-mail address(es)."), "errorcode" => "invalid_to_address", "info" => $toaddr);
|
|
if (!self::EmailAddressesToNamesAndEmail($tempfromnames, $tempfromaddrs, $fromaddr, true, $options)) return array("success" => false, "error" => self::SMTP_Translate("Invalid 'From' e-mail address."), "errorcode" => "invalid_from_address", "info" => $fromaddr);
|
|
|
|
$server = (isset($options["server"]) ? $options["server"] : "localhost");
|
|
$secure = (isset($options["secure"]) ? $options["secure"] : false);
|
|
$port = (isset($options["port"]) ? (int)$options["port"] : -1);
|
|
if ($port < 0 || $port > 65535) $port = ($secure ? 465 : 25);
|
|
$debug = (isset($options["debug"]) ? $options["debug"] : false);
|
|
|
|
$headers = "Message-ID: <" . self::SMTP_RandomHexString(8) . "." . self::SMTP_RandomHexString(7) . "@" . substr($tempfromaddrs[0], strrpos($tempfromaddrs[0], "@") + 1) . ">\r\n";
|
|
$headers .= "Date: " . date("D, d M Y H:i:s O") . "\r\n";
|
|
|
|
$message = $headers . $message;
|
|
$message = self::ReplaceNewlines("\r\n", $message);
|
|
$message = str_replace("\r\n.\r\n", "\r\n..\r\n", $message);
|
|
|
|
// Set up the final output array.
|
|
$result = array("success" => true, "rawsendsize" => 0, "rawrecvsize" => 0, "startts" => $startts);
|
|
$debug = (isset($options["debug"]) && $options["debug"]);
|
|
if ($debug)
|
|
{
|
|
$result["rawsend"] = "";
|
|
$result["rawrecv"] = "";
|
|
}
|
|
|
|
if ($timeout !== false && self::GetTimeLeft($startts, $timeout) == 0) return array("success" => false, "error" => self::SMTP_Translate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");
|
|
|
|
// Connect to the target server.
|
|
$hostname = (isset($options["hostname"]) ? $options["hostname"] : "[" . trim(isset($_SERVER["SERVER_ADDR"]) && $_SERVER["SERVER_ADDR"] != "127.0.0.1" ? $_SERVER["SERVER_ADDR"] : "192.168.0.101") . "]");
|
|
$errornum = 0;
|
|
$errorstr = "";
|
|
if (isset($options["fp"]) && is_resource($options["fp"]))
|
|
{
|
|
$fp = $options["fp"];
|
|
unset($options["fp"]);
|
|
}
|
|
else
|
|
{
|
|
if (!isset($options["connecttimeout"])) $options["connecttimeout"] = 10;
|
|
$timeleft = self::GetTimeLeft($startts, $timeout);
|
|
if ($timeleft !== false) $options["connecttimeout"] = min($options["connecttimeout"], $timeleft);
|
|
if (!function_exists("stream_socket_client")) $fp = @fsockopen(($secure ? "tls://" : "") . $server, $port, $errornum, $errorstr, $options["connecttimeout"]);
|
|
else
|
|
{
|
|
$context = @stream_context_create();
|
|
if (isset($options["source_ip"])) $context["socket"] = array("bindto" => $options["source_ip"] . ":0");
|
|
if ($secure && isset($options["sslopts"]) && is_array($options["sslopts"]))
|
|
{
|
|
self::ProcessSSLOptions($options, "sslopts", $server);
|
|
foreach ($options["sslopts"] as $key => $val) @stream_context_set_option($context, "ssl", $key, $val);
|
|
}
|
|
$fp = @stream_socket_client(($secure ? "tls://" : "") . $server . ":" . $port, $errornum, $errorstr, $options["connecttimeout"], STREAM_CLIENT_CONNECT | (isset($options["async"]) && $options["async"] ? STREAM_CLIENT_ASYNC_CONNECT : 0), $context);
|
|
}
|
|
|
|
if ($fp === false) return array("success" => false, "error" => self::SMTP_Translate("Unable to establish a SMTP connection to '%s'.", ($secure ? "tls://" : "") . $server . ":" . $port), "errorcode" => "connection_failure", "info" => $errorstr . " (" . $errornum . ")");
|
|
}
|
|
|
|
if (function_exists("stream_set_blocking")) @stream_set_blocking($fp, (isset($options["async"]) && $options["async"] ? 0 : 1));
|
|
|
|
// Initialize the connection request state array.
|
|
$state = array(
|
|
"fp" => $fp,
|
|
"async" => (isset($options["async"]) ? $options["async"] : false),
|
|
"debug" => $debug,
|
|
"startts" => $startts,
|
|
"timeout" => $timeout,
|
|
"waituntil" => -1.0,
|
|
"data" => "",
|
|
"code" => 0,
|
|
"expectedcode" => 0,
|
|
"expectederror" => "",
|
|
"response" => "",
|
|
"fromaddrs" => $tempfromaddrs,
|
|
"toaddrs" => $temptoaddrs,
|
|
"message" => $message,
|
|
"secure" => $secure,
|
|
|
|
"state" => "connecting",
|
|
|
|
"options" => $options,
|
|
"result" => $result
|
|
);
|
|
|
|
// Return the state for async calls. Caller must call ProcessState().
|
|
if ($state["async"]) return array("success" => true, "state" => $state);
|
|
|
|
// Run through all of the valid states and return the result.
|
|
return self::ProcessState($state);
|
|
}
|
|
|
|
// Has to be public so that TagFilter can successfully call.
|
|
public static function SMTP_HTMLTagFilter($stack, &$content, $open, $tagname, &$attrs, $options)
|
|
{
|
|
$content = str_replace(array(" ", " ", "\xC2\xA0"), array(" ", " ", " "), $content);
|
|
$content = str_replace("&", "&", $content);
|
|
$content = str_replace(""", "\"", $content);
|
|
|
|
if ($tagname === "head") return array("keep_tag" => false, "keep_interior" => false);
|
|
if ($tagname === "style") return array("keep_tag" => false, "keep_interior" => false);
|
|
if ($tagname === "script") return array("keep_tag" => false, "keep_interior" => false);
|
|
if ($tagname === "a" && (!isset($attrs["href"]) || trim($attrs["href"]) === "")) return array("keep_tag" => false, "keep_interior" => false);
|
|
if ($tagname === "/a" && $stack[0]["keep_interior"])
|
|
{
|
|
if ($stack[0]["attrs"]["href"] === trim($content)) $content = " [ " . trim($content) . " ] ";
|
|
else if (trim($content) !== "") $content = " " . trim($content) . " (" . trim($stack[0]["attrs"]["href"]) . ") ";
|
|
}
|
|
if ($tagname === "img")
|
|
{
|
|
if (!isset($attrs["src"])) $attrs["src"] = "";
|
|
|
|
if (isset($attrs["alt"]) && trim($attrs["alt"]) !== "" && trim($attrs["alt"]) !== $attrs["src"]) $content .= trim($attrs["alt"]) . "\n\n";
|
|
}
|
|
|
|
if ($tagname === "table" || $tagname === "blockquote" || $tagname === "ul") self::$depths[] = $tagname;
|
|
if ($tagname === "ol") self::$depths[] = 1;
|
|
|
|
if (trim($content) !== "")
|
|
{
|
|
if ($tagname === "/tr") $content = ltrim($content) . "\n\n";
|
|
if ($tagname === "/th") $content = "*" . ltrim($content) . "*\n";
|
|
if ($tagname === "/td") $content = ltrim($content) . "\n";
|
|
if ($tagname === "/div") $content = ltrim($content) . "\n";
|
|
if ($tagname === "/li") $content = "\n" . (count(self::$depths) && is_int(self::$depths[count(self::$depths) - 1]) ? sprintf("%d. ", self::$depths[count(self::$depths) - 1]++) : "- ") . ltrim($content) . "\n";
|
|
if ($tagname === "br") $content .= "\n";
|
|
if ($tagname === "/h1" || $tagname === "/h2" || $tagname === "/h3") $content = "*" . trim($content) . "*\n\n";
|
|
if ($tagname === "/h4" || $tagname === "/h5" || $tagname === "/h6") $content = "*" . trim($content) . "*\n";
|
|
if ($tagname === "/i" || $tagname === "/em") $content = " _" . trim($content) . "_ ";
|
|
if ($tagname === "/b" || $tagname === "/strong") $content = " *" . trim($content) . "* ";
|
|
if ($tagname === "/p") $content = "\n\n" . trim($content) . "\n\n";
|
|
if ($tagname === "/blockquote") $content = "------------------------\n" . trim($content) . "\n------------------------\n";
|
|
if ($tagname === "/ul" || $tagname === "/ol" || $tagname === "/table" || $tagname === "/blockquote")
|
|
{
|
|
// Indent the lines of content varying amounts depending on final depth.
|
|
$prefix = "";
|
|
if ($tagname === "/table") $prefix .= "\xFF\xFF";
|
|
if ($tagname === "/ul" || $tagname === "/ol") $prefix .= "\xFF\xFF" . (count(self::$depths) > 1 ? "\xFF\xFF" : "");
|
|
if ($tagname === "/blockquote") $prefix .= "\xFF\xFF\xFF\xFF";
|
|
|
|
$lines = explode("\n", $content);
|
|
foreach ($lines as $num => $line)
|
|
{
|
|
if (trim($line) !== "")
|
|
{
|
|
if ($line{0} !== "\xFF" && (($tagname === "/ul" && $line{0} !== "-") || ($tagname === "/ol" && !(int)$line{0}))) $prefix2 = "\xFF\xFF";
|
|
else $prefix2 = "";
|
|
|
|
$lines[$num] = $prefix . $prefix2 . trim($line);
|
|
}
|
|
}
|
|
$content = "\n\n" . implode("\n", $lines) . "\n\n";
|
|
}
|
|
if ($tagname === "/pre") $content = "\n\n" . $content . "\n\n";
|
|
}
|
|
|
|
if ($tagname === "/table" || $tagname === "/blockquote" || $tagname === "/ul" || $tagname === "/ol") array_pop(self::$depths);
|
|
|
|
return array("keep_tag" => false);
|
|
}
|
|
|
|
// Has to be public so that TagFilter can successfully call.
|
|
public static function SMTP_HTMLContentFilter($stack, $result, &$content, $options)
|
|
{
|
|
if (TagFilter::GetParentPos($stack, "pre") === false)
|
|
{
|
|
$content = preg_replace('/\s{2,}/', " ", str_replace(array("\r\n", "\n", "\r", "\t"), " ", $content));
|
|
if ($result !== "" && substr($result, -1) === "\n") $content = trim($content);
|
|
}
|
|
}
|
|
|
|
public static function ConvertHTMLToText($data)
|
|
{
|
|
self::$depths = array();
|
|
|
|
// Load TagFilter.
|
|
if (!class_exists("TagFilter")) require_once str_replace("\\", "/", dirname(__FILE__)) . "/tag_filter.php";
|
|
|
|
$data = UTF8::MakeValid($data);
|
|
|
|
$options = TagFilter::GetHTMLOptions();
|
|
$options["tag_callback"] = "SMTP::SMTP_HTMLTagFilter";
|
|
$options["content_callback"] = "SMTP::SMTP_HTMLContentFilter";
|
|
|
|
$data = TagFilter::Run($data, $options);
|
|
|
|
$data = str_replace("\xFF", " ", $data);
|
|
|
|
$data = UTF8::MakeValid($data);
|
|
|
|
return $data;
|
|
}
|
|
|
|
private static function MIME_RandomString($length)
|
|
{
|
|
$lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
$result = "";
|
|
|
|
while ($length)
|
|
{
|
|
$result .= $lookup[mt_rand(0, 61)];
|
|
|
|
$length--;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function SendEmail($fromaddr, $toaddr, $subject, $options = array())
|
|
{
|
|
$subject = str_replace("\r", " ", $subject);
|
|
$subject = str_replace("\n", " ", $subject);
|
|
if (!UTF8::IsASCII($subject)) $subject = self::ConvertToRFC1342($subject);
|
|
|
|
$replytoaddr = (isset($options["replytoaddr"]) ? $options["replytoaddr"] : "");
|
|
$ccaddr = (isset($options["ccaddr"]) ? $options["ccaddr"] : "");
|
|
$bccaddr = (isset($options["bccaddr"]) ? $options["bccaddr"] : "");
|
|
$headers = (isset($options["headers"]) ? $options["headers"] : "");
|
|
$textmessage = (isset($options["textmessage"]) ? $options["textmessage"] : "");
|
|
$htmlmessage = (isset($options["htmlmessage"]) ? $options["htmlmessage"] : "");
|
|
$attachments = (isset($options["attachments"]) ? $options["attachments"] : array());
|
|
|
|
$messagetoaddr = self::EmailAddressesToEmailHeaders($toaddr, "To", true, false, $options);
|
|
$replytoaddr = self::EmailAddressesToEmailHeaders($replytoaddr, "Reply-To", false, false, $options);
|
|
if ($replytoaddr == "") $replytoaddr = self::EmailAddressesToEmailHeaders($fromaddr, "Reply-To", false, false, $options);
|
|
$messagefromaddr = self::EmailAddressesToEmailHeaders($fromaddr, "From", false, false, $options);
|
|
if ($messagefromaddr == "" && $replytoaddr == "") return array("success" => false, "error" => self::SMTP_Translate("From address is invalid."), "errorcode" => "invalid_from_address", "info" => $fromaddr);
|
|
if ($ccaddr != "") $toaddr .= ", " . $ccaddr;
|
|
$ccaddr = self::EmailAddressesToEmailHeaders($ccaddr, "Cc", true, false, $options);
|
|
if ($bccaddr != "") $toaddr .= ", " . $bccaddr;
|
|
$bccaddr = self::EmailAddressesToEmailHeaders($bccaddr, "Bcc", true, false, $options);
|
|
|
|
if ($htmlmessage == "" && !count($attachments))
|
|
{
|
|
// Plain-text e-mail.
|
|
$destheaders = "";
|
|
$destheaders .= $messagefromaddr;
|
|
if ($headers != "") $destheaders .= $headers;
|
|
$destheaders .= "MIME-Version: 1.0\r\n";
|
|
if (!isset($options["usemail"]) || !$options["usemail"]) $destheaders .= $messagetoaddr;
|
|
if ($replytoaddr != "") $destheaders .= $replytoaddr;
|
|
if ($ccaddr != "") $destheaders .= $ccaddr;
|
|
if ($bccaddr != "") $destheaders .= $bccaddr;
|
|
if (!isset($options["usemail"]) || !$options["usemail"]) $destheaders .= "Subject: " . $subject . "\r\n";
|
|
$destheaders .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$destheaders .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
|
|
$message = self::ConvertEmailMessageToRFC1341($textmessage);
|
|
}
|
|
else
|
|
{
|
|
// MIME e-mail (HTML, text, attachments).
|
|
$mimeboundary = "--------" . self::MIME_RandomString(25);
|
|
$destheaders = "";
|
|
$destheaders .= $messagefromaddr;
|
|
if ($headers != "") $destheaders .= $headers;
|
|
$destheaders .= "MIME-Version: 1.0\r\n";
|
|
if (!isset($options["usemail"]) || !$options["usemail"]) $destheaders .= $messagetoaddr;
|
|
if ($replytoaddr != "") $destheaders .= $replytoaddr;
|
|
if ($ccaddr != "") $destheaders .= $ccaddr;
|
|
if ($bccaddr != "") $destheaders .= $bccaddr;
|
|
if (!isset($options["usemail"]) || !$options["usemail"]) $destheaders .= "Subject: " . $subject . "\r\n";
|
|
if (count($attachments) && isset($options["inlineattachments"]) && $options["inlineattachments"]) $destheaders .= "Content-Type: multipart/related; boundary=\"" . $mimeboundary . "\"; type=\"multipart/alternative\"\r\n";
|
|
else if (count($attachments)) $destheaders .= "Content-Type: multipart/mixed; boundary=\"" . $mimeboundary . "\"\r\n";
|
|
else if ($textmessage != "" && $htmlmessage != "") $destheaders .= "Content-Type: multipart/alternative; boundary=\"" . $mimeboundary . "\"\r\n";
|
|
else $mimeboundary = "";
|
|
|
|
if ($mimeboundary != "") $mimecontent = "This is a multi-part message in MIME format.\r\n";
|
|
else $mimecontent = "";
|
|
|
|
if ($textmessage == "" || $htmlmessage == "" || !count($attachments)) $mimeboundary2 = $mimeboundary;
|
|
else
|
|
{
|
|
$mimeboundary2 = "--------" . self::MIME_RandomString(25);
|
|
$mimecontent .= "--" . $mimeboundary . "\r\n";
|
|
$mimecontent .= "Content-Type: multipart/alternative; boundary=\"" . $mimeboundary2 . "\"\r\n";
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
|
|
if ($textmessage != "")
|
|
{
|
|
if ($mimeboundary2 != "")
|
|
{
|
|
$mimecontent .= "--" . $mimeboundary2 . "\r\n";
|
|
$mimecontent .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$mimecontent .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
else
|
|
{
|
|
$destheaders .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$destheaders .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
}
|
|
$message = self::ConvertEmailMessageToRFC1341($textmessage);
|
|
$mimecontent .= $message;
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
|
|
if ($htmlmessage != "")
|
|
{
|
|
if ($mimeboundary2 != "")
|
|
{
|
|
$mimecontent .= "--" . $mimeboundary2 . "\r\n";
|
|
$mimecontent .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
$mimecontent .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
else
|
|
{
|
|
$destheaders .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
$destheaders .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
}
|
|
$message = self::ConvertEmailMessageToRFC1341($htmlmessage);
|
|
$mimecontent .= $message;
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
|
|
if ($mimeboundary2 != "" && $mimeboundary != $mimeboundary2) $mimecontent .= "--" . $mimeboundary2 . "--\r\n";
|
|
|
|
// Process the attachments.
|
|
$y = count($attachments);
|
|
for ($x = 0; $x < $y; $x++)
|
|
{
|
|
$mimecontent .= "--" . $mimeboundary . "\r\n";
|
|
$type = str_replace("\r", "", $attachments[$x]["type"]);
|
|
$type = str_replace("\n", "", $type);
|
|
$type = UTF8::ConvertToASCII($type);
|
|
if (!isset($attachments[$x]["name"])) $name = "";
|
|
else
|
|
{
|
|
$name = str_replace("\r", "", $attachments[$x]["name"]);
|
|
$name = str_replace("\n", "", $name);
|
|
$name = self::FilenameSafe($name);
|
|
}
|
|
|
|
if (!isset($attachments[$x]["location"])) $location = "";
|
|
else
|
|
{
|
|
$location = str_replace("\r", "", $attachments[$x]["location"]);
|
|
$location = str_replace("\n", "", $location);
|
|
$location = UTF8::ConvertToASCII($location);
|
|
}
|
|
|
|
if (!isset($attachments[$x]["cid"])) $cid = "";
|
|
else
|
|
{
|
|
$cid = str_replace("\r", "", $attachments[$x]["cid"]);
|
|
$cid = str_replace("\n", "", $cid);
|
|
$cid = UTF8::ConvertToASCII($cid);
|
|
}
|
|
$mimecontent .= "Content-Type: " . $type . ($name != "" ? "; name=\"" . $name . "\"" : "") . "\r\n";
|
|
if ($cid != "") $mimecontent .= "Content-ID: <" . $cid . ">\r\n";
|
|
if ($location != "") $mimecontent .= "Content-Location: " . $location . "\r\n";
|
|
$mimecontent .= "Content-Transfer-Encoding: base64\r\n";
|
|
if ($name != "") $mimecontent .= "Content-Disposition: inline; filename=\"" . $name . "\"\r\n";
|
|
$mimecontent .= "\r\n";
|
|
$mimecontent .= chunk_split(base64_encode($attachments[$x]["data"]));
|
|
$mimecontent .= "\r\n";
|
|
}
|
|
|
|
if ($mimeboundary != "") $mimecontent .= "--" . $mimeboundary . "--\r\n";
|
|
$message = $mimecontent;
|
|
}
|
|
|
|
if (isset($options["returnresults"]) && $options["returnresults"]) return array("success" => true, "toaddr" => $toaddr, "fromaddr" => $fromaddr, "headers" => $destheaders, "subject" => $subject, "message" => $message);
|
|
else if (isset($options["usemail"]) && $options["usemail"])
|
|
{
|
|
$result = mail($toaddr, $subject, self::ReplaceNewlines("\n", $message), $destheaders);
|
|
if (!$result) return array("success" => false, "error" => self::SMTP_Translate("PHP mail() call failed."), "errorcode" => "mail_call_failed");
|
|
|
|
return array("success" => true);
|
|
}
|
|
else
|
|
{
|
|
return self::SendSMTPEmail($toaddr, $fromaddr, $destheaders . "\r\n" . $message, $options);
|
|
}
|
|
}
|
|
}
|
|
?>
|