5 # Part of the ScoutLib application support library 6 # Copyright 2012-2015 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu 17 # ---- PUBLIC INTERFACE -------------------------------------------------- 31 if (count(self::$RecipientWhitelist))
33 # save recipient list and then pare it down based on whitelist 36 foreach ($this->
To as $To)
38 foreach (self::$RecipientWhitelist as $White)
40 $White = trim($White);
43 $White =
"/".preg_quote($White,
"/").
"/";
45 if (preg_match($White, $To))
55 # if there are recipients 59 switch (self::$DeliveryMethod)
61 case self::METHOD_PHPMAIL:
62 # use PHPMailer to send multipart alternative messages because 63 # they can be tricky to construct properly 64 if ($this->HasAlternateBody())
66 $Result = $this->SendViaPhpMailerLib();
69 # otherwise, just use the built-in mail() function 72 $Result = $this->SendViaPhpMailFunc();
76 case self::METHOD_SMTP:
77 $Result = $this->SendViaSmtp();
81 # log if a message was sent 82 if ($Result && self::$LoggingFunc !== NULL)
84 call_user_func(self::$LoggingFunc, $this, $this->LogData);
88 # if recipient list saved 91 # restore recipient list 95 # report to caller whether message was sent 109 if ($NewValue !== NULL)
111 self::$RecipientWhitelist = $NewValue;
113 return self::$RecipientWhitelist;
123 if (!is_callable($NewValue))
126 "Invalid logging function provided.");
128 self::$LoggingFunc = $NewValue;
138 $this->LogData = $LogData;
148 public function Body($NewValue = NULL)
150 if ($NewValue !== NULL) { $this->
Body = $NewValue; }
161 # set the plain-text alternative if a parameter is given 162 if (func_num_args() > 0)
167 return $this->AlternateBody;
177 if ($NewValue !== NULL) { $this->
Subject = $NewValue; }
178 return $this->Subject;
189 public function From($NewAddress = NULL, $NewName = NULL)
191 if ($NewAddress !== NULL)
193 $NewAddress = trim($NewAddress);
194 if ($NewName !== NULL)
196 $NewName = trim($NewName);
197 $this->
From = $NewName.
" <".$NewAddress.
">";
201 $this->
From = $NewAddress;
215 if ($NewValue !== NULL) { self::$DefaultFrom = $NewValue; }
216 return self::$DefaultFrom;
227 public function ReplyTo($NewAddress = NULL, $NewName = NULL)
229 if ($NewAddress !== NULL)
231 $NewAddress = trim($NewAddress);
232 if ($NewName !== NULL)
234 $NewName = trim($NewName);
235 $this->
ReplyTo = $NewName.
" <".$NewAddress.
">";
242 return $this->ReplyTo;
252 public function To($NewValue = NULL)
254 if ($NewValue !== NULL)
256 if (!is_array($NewValue))
258 $this->
To = array($NewValue);
262 $this->
To = $NewValue;
275 public function CC($NewValue = NULL)
277 if ($NewValue !== NULL)
279 if (!is_array($NewValue))
281 $this->
CC = array($NewValue);
285 $this->
CC = $NewValue;
298 public function BCC($NewValue = NULL)
300 if ($NewValue !== NULL)
302 if (!is_array($NewValue))
304 $this->
BCC = array($NewValue);
308 $this->
BCC = $NewValue;
320 # add new headers to list 321 $this->Headers = array_merge($this->Headers, $NewHeaders);
332 # set the plain-text alternative if a parameter is given 333 if (func_num_args() > 0)
338 return $this->CharSet;
348 if (!is_null($NewValue))
350 self::$LineEnding = $NewValue;
353 return self::$LineEnding;
369 $Html, $MaxLineLength=998, $LineEnding=
"\r\n")
371 # the regular expression used to find long lines 372 $LongLineRegExp =
'/[^\r\n]{'.($MaxLineLength+1).
',}/';
374 # find all lines that are too long 375 preg_match_all($LongLineRegExp, $Html, $Matches,
376 PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
378 # no changes are necessary 379 if (!count($Matches))
384 # go backwards so that the HTML can be edited in place without messing 386 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
388 # extract the line text and its offset within the string 389 list($Line, $Offset) = $Matches[0][$i];
391 # first try to get the line under the limit without being too 393 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
394 $WasAggressive =
"No";
396 # if the line is still too long, be more aggressive with replacing 397 # horizontal whitespace 398 if (preg_match($LongLineRegExp, $BetterLine))
400 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
401 $WasAggressive =
"Yes";
404 # tack on an HTML comment stating that the line was wrapped and give 405 # some additional info 406 $BetterLine = $LineEnding.
"<!-- Line was wrapped. Aggressive: " 407 .$WasAggressive.
", Max: ".$MaxLineLength.
", Actual: " 408 .strlen($Line).
" -->".$LineEnding.$BetterLine;
410 # replace the line within the HTML 411 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
426 # the number of \r in the string 427 $NumCR = substr_count($Value,
"\r");
430 if ($LineEnding ==
"\n")
435 # the number of \n in the string 436 $NumLF = substr_count($Value,
"\n");
439 if ($LineEnding ==
"\r")
444 # the number of \r\n in the string 445 $NumCRLF = substr_count($Value,
"\r\n");
447 # CRLF. also check CRLF to make sure CR and LF appear together and in 449 return $NumCR === $NumLF && $NumLF === $NumCRLF;
460 $Text = str_replace(array(
"\r",
"\n"),
"", $Html);
462 # convert HTML breaks to newlines 463 $Text = preg_replace(
'/<br\s*\/?>/',
"\n", $Text);
465 # strip remaining tags 466 $Text = strip_tags($Text);
468 # convert HTML entities to their plain-text equivalents 469 $Text = html_entity_decode($Text);
471 # single quotes aren't always handled 472 $Text = str_replace(
''',
"'", $Text);
474 # remove HTML entities that have no equivalents 475 $Text = preg_replace(
'/&(#[0-9]{1,6}|[a-zA-Z0-9]{1,6});/',
"", $Text);
477 # return the plain text version 491 if ($NewValue !== NULL)
493 self::$DeliveryMethod = $NewValue;
495 return self::$DeliveryMethod;
507 public static function Server($NewValue = NULL)
509 if ($NewValue !== NULL) { self::$Server = $NewValue; }
510 return self::$Server;
518 public static function Port($NewValue = NULL)
520 if ($NewValue !== NULL) { self::$Port = $NewValue; }
531 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
532 return self::$UserName;
542 if ($NewValue !== NULL) { self::$Password = $NewValue; }
543 return self::$Password;
553 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
554 return self::$UseAuthentication;
566 if ($NewSettings !== NULL)
568 $Settings = unserialize($NewSettings);
569 self::$DeliveryMethod = $Settings[
"DeliveryMethod"];
570 self::$Server = $Settings[
"Server"];
571 self::$Port = $Settings[
"Port"];
572 self::$UserName = $Settings[
"UserName"];
573 self::$Password = $Settings[
"Password"];
574 self::$UseAuthentication = $Settings[
"UseAuthentication"];
578 $Settings[
"DeliveryMethod"] = self::$DeliveryMethod;
579 $Settings[
"Server"] = self::$Server;
580 $Settings[
"Port"] = self::$Port;
581 $Settings[
"UserName"] = self::$UserName;
582 $Settings[
"Password"] = self::$Password;
583 $Settings[
"UseAuthentication"] = self::$UseAuthentication;
585 return serialize($Settings);
598 # start out with error list clear 599 self::$DeliverySettingErrorList = array();
601 # test based on delivery method 602 switch (self::$DeliveryMethod)
604 case self::METHOD_PHPMAIL:
605 # always report success 606 $SettingsOkay = TRUE;
609 case self::METHOD_SMTP:
610 # set up PHPMailer for test 611 $PMail =
new PHPMailer(TRUE);
613 $PMail->SMTPAuth = self::$UseAuthentication;
614 $PMail->Host = self::$Server;
615 $PMail->Port = self::$Port;
616 $PMail->Username = self::$UserName;
617 $PMail->Password = self::$Password;
622 $SettingsOkay = $PMail->SmtpConnect();
625 catch (phpmailerException $Except)
627 # translate PHPMailer error message to possibly bad settings 628 switch ($Except->getMessage())
630 case 'SMTP Error: Could not authenticate.':
631 self::$DeliverySettingErrorList = array(
638 case 'SMTP Error: Could not connect to SMTP host.':
639 self::$DeliverySettingErrorList = array(
645 case 'Language string failed to load: tls':
646 self::$DeliverySettingErrorList = array(
"TLS");
650 self::$DeliverySettingErrorList = array(
"UNKNOWN");
654 # make sure failure is reported 655 $SettingsOkay = FALSE;
660 # report result to caller 661 return $SettingsOkay;
670 return self::$DeliverySettingErrorList;
674 # ---- PRIVATE INTERFACE ------------------------------------------------- 676 private $AlternateBody =
"";
677 private $BCC = array();
679 private $CC = array();
682 private $Headers = array();
683 private $ReplyTo =
"";
684 private $Subject =
"";
685 private $To = array();
686 private $Whitelist = array();
687 private $LogData = NULL;
689 private static $DefaultFrom =
"";
690 private static $DeliveryMethod = self::METHOD_PHPMAIL;
691 private static $DeliverySettingErrorList = array();
692 private static $LineEnding =
"\r\n";
693 private static $Password =
"";
694 private static $Port = 25;
695 private static $RecipientWhitelist = array();
696 private static $Server;
697 private static $UseAuthentication = FALSE;
698 private static $UserName =
"";
699 private static $LoggingFunc = NULL;
705 private function SendViaPhpMailFunc()
707 # Contrary to the PHP documentation, line endings for PHP's 708 # mail function should be the system native line endings. 710 # see https://bugs.php.net/bug.php?id=15841 for details 712 # Use the system line endings 715 # build basic headers list 716 $From = strlen($this->
From) ? $this->
From : self::$DefaultFrom;
717 $Headers =
"From: ".self::CleanHeaderValue($From).$LE;
718 $Headers .= $this->BuildAddresseeLine(
"Cc", $this->
CC);
719 $Headers .= $this->BuildAddresseeLine(
"Bcc", $this->
BCC);
720 $Headers .=
"Reply-To: ".self::CleanHeaderValue(
723 # add additional headers 724 foreach ($this->Headers as $ExtraHeader)
726 $Headers .= $ExtraHeader.$LE;
729 # build recipient list 732 foreach ($this->
To as $Recipient)
734 $To .= $Separator.$Recipient;
738 # normalize message body line endings 739 $Body = $this->NormalizeLineEndings($this->
Body, $LE);
742 $Result = mail($To, $this->
Subject, $Body, $Headers);
744 # report to caller whether attempt to send succeeded 753 private function SendViaPhpMailerLib()
755 # create and initialize PHPMailer 756 $PMail =
new PHPMailer();
757 $PMail->LE = self::$LineEnding;
758 $PMail->Subject = $this->Subject;
759 $PMail->Body = $this->Body;
760 $PMail->IsHTML(FALSE);
762 # default values for the sender's name and address 764 $Address = $this->From;
766 # if the address contains a name and address, they need to extracted 767 # because PHPMailer requires that they are set as two different 769 if (preg_match(
"/ </", $this->
From))
771 $Pieces = explode(
" ", $this->
From);
772 $Address = array_pop($Pieces);
773 $Address = preg_replace(
"/[<>]+/",
"", $Address);
774 $Name = trim(implode($Pieces,
" "));
778 $PMail->SetFrom($Address, $Name);
781 foreach ($this->
To as $Recipient)
783 $PMail->AddAddress($Recipient);
786 # add any extra header lines 787 foreach ($this->Headers as $ExtraHeader)
789 $PMail->AddCustomHeader($ExtraHeader);
792 # add the charset if it's set 795 $PMail->CharSet = strtolower($this->
CharSet);
798 # add the alternate plain-text body if it's set 799 if ($this->HasAlternateBody())
801 $PMail->AltBody = $this->AlternateBody;
804 # set up SMTP if necessary 805 if (self::$DeliveryMethod == self::METHOD_SMTP)
808 $PMail->SMTPAuth = self::$UseAuthentication;
809 $PMail->Host = self::$Server;
810 $PMail->Port = self::$Port;
811 $PMail->Username = self::$UserName;
812 $PMail->Password = self::$Password;
816 $Result = $PMail->Send();
818 # report to caller whether attempt to send succeeded 826 private function SendViaSmtp()
828 # send via PHPMailer because it's capable of handling SMTP 829 return $this->SendViaPhpMailerLib();
838 private function BuildAddresseeLine($Label, $Recipients)
841 if (count($Recipients))
843 $Line .= $Label.
": ";
845 foreach ($Recipients as $Recipient)
847 $Line .= $Separator.self::CleanHeaderValue($Recipient);
850 $Line .= self::$LineEnding;
859 private function HasAlternateBody()
869 private static function CleanHeaderValue($Value)
871 # (regular expression taken from sanitizeHeaders() function in 873 return preg_replace(
'=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
883 private static function NormalizeLineEndings($Value, $LineEnding)
885 return preg_replace(
'/\r\n|\r|\n/', $LineEnding, $Value);
903 $Html, $Aggressive=FALSE, $LineEnding=
"\r\n")
905 $HtmlLength = strlen($Html);
907 # tags that should have their inner HTML left alone 908 $IgnoredTags = array(
'script',
'style',
'textarea',
'title');
910 # values for determining context 912 $InClosingTag = FALSE;
913 $InIgnoredTag = FALSE;
914 $InAttribute = FALSE;
916 $IgnoredTagName = NULL;
917 $AttributeDelimiter = NULL;
919 # loop through each character of the string 920 for ($i = 0; $i < $HtmlLength; $i++)
925 if ($Char ==
"<" && !$InTag)
928 $InAttribute = FALSE;
929 $AttributeDelimiter = NULL;
931 # do some lookaheads to get the tag name and to see if the tag 933 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
935 # moving into an ignored tag 936 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
938 $InIgnoredTag = TRUE;
939 $IgnoredTagName = $TagName;
946 if ($Char ==
">" && $InTag && !$InAttribute)
948 # moving out of an ignored tag 949 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
951 $InIgnoredTag = FALSE;
952 $IgnoredTagName = NULL;
956 $InClosingTag = FALSE;
957 $InAttribute = FALSE;
959 $AttributeDelimiter = NULL;
964 # attribute delimiter characters 965 if ($Char ==
"'" || $Char ==
'"')
967 # beginning of an attribute 971 $AttributeDelimiter = $Char;
975 # end of the attribute 976 if ($InAttribute && $Char == $AttributeDelimiter)
978 $InAttribute = FALSE;
979 $AttributeDelimiter = NULL;
984 # whitespace inside of a tag but outside of an attribute can be 985 # safely converted to a newline 986 if ($InTag && !$InAttribute && preg_match(
'/\s/', $Char))
988 $Html{$i} = $LineEnding;
992 # whitespace outside of a tag can be safely converted to a newline 993 # when not in one of the ignored tags, but only do so if horizontal 994 # space is at a premium because it can make the resulting HTML 996 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match(
'/\s/', $Char))
998 $Html{$i} = $LineEnding;
1016 $HtmlLength = strlen($Html);
1018 # default return values 1019 $InClosingTag = FALSE;
1022 # if at the end of the string and lookaheads aren't possible 1023 if ($TagBegin + 1 >= $HtmlLength)
1025 return array($InClosingTag, $TagName);
1028 # do a lookahead for whether it's a closing tag 1029 if ($Html{$TagBegin+1} ==
"/")
1031 $InClosingTag = TRUE;
1034 # determine whether to offset by one or two to get the tag name 1035 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
1037 # do a lookahead for the tag name 1038 for ($i = $TagStart; $i < $HtmlLength; $i++)
1042 # stop getting the tag name if whitespace is found and something is 1043 # available for the tag name 1044 if (strlen($TagName) && preg_match(
'/[\r\n\s]/', $Char))
1049 # stop getting the tag name if the character is > 1059 if (substr($TagName, 0, 3) ==
"!--")
1061 return array($InClosingTag,
"!--");
1064 # remove characters that aren't part of a valid tag name 1065 $TagName = preg_replace(
'/[^a-zA-Z0-9]/',
'', $TagName);
1067 return array($InClosingTag, $TagName);
From($NewAddress=NULL, $NewName=NULL)
Get/set message sender.
static RegisterLoggingFunction($NewValue)
Register a logging callback.
static Server($NewValue=NULL)
Get/set server for mail delivery.
static DeliveryMethod($NewValue=NULL)
Get/set mail delivery method.
static WrapHtmlAsNecessary($Html, $MaxLineLength=998, $LineEnding="\r\n")
Wrap HTML in an e-mail as necessary to get its lines less than some max length.
To($NewValue=NULL)
Get/set message recipient(s).
static DeliverySettings($NewSettings=NULL)
Get/set serialized (opaque text) version of delivery settings.
ReplyTo($NewAddress=NULL, $NewName=NULL)
Get/set message "Reply-To" address.
static ToWhitelist($NewValue=NULL)
Set whitelist of acceptable recipient addresses.
static DeliverySettingsOkay()
Test delivery settings and report their validity.
static LineEnding($NewValue=NULL)
Specify the character sequence that should be used to end lines.
static DeliverySettingErrors()
Return array with list of delivery setting errors (if any).
CharSet($NewValue=NULL)
Specify a character encoding for the message.
static DefaultFrom($NewValue=NULL)
Get/set default "From" address.
static GetTagInfo($Html, $TagBegin)
Get the tag name and whether it's a closing tag from a tag that begins at a specific offset within so...
static substr()
Multibyte-aware (if supported in PHP) version of substr().
AddHeaders($NewHeaders)
Specify additional message headers to be included.
AddLogData($LogData)
Provide additional data that should be included when a message is logged.
const METHOD_PHPMAIL
Deliver using PHP's internal mail() mechanism.
BCC($NewValue=NULL)
Get/set message BCC list.
Body($NewValue=NULL)
Get/set message body.
static Port($NewValue=NULL)
Get/set port number for mail delivery.
static UserName($NewValue=NULL)
Get/set user name for mail delivery.
AlternateBody($NewValue=NULL)
Get/set the plain-text alternative to the body.
Subject($NewValue=NULL)
Get/set message subject.
const METHOD_SMTP
Deliver using SMTP.
static Password($NewValue=NULL)
Get/set password for mail delivery.
static ConvertHtmlToPlainText($Html)
Try as best as possible to convert HTML to plain text.
static UseAuthentication($NewValue=NULL)
Get/set whether to use authentication for mail delivery.
static TestLineEndings($Value, $LineEnding)
Test the line endings in a value to see if they all match the given line ending.
CC($NewValue=NULL)
Get/set message CC list.
static ConvertHtmlWhiteSpace($Html, $Aggressive=FALSE, $LineEnding="\r\n")
Convert horizontal white space with no semantic value to vertical white space when possible...