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);
41 if ($White[0] != substr($White, 0, -1))
43 $White =
"/".$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();
82 # if recipient list saved 85 # restore recipient list 89 # report to caller whether message was sent 103 if ($NewValue !== NULL)
105 self::$RecipientWhitelist = $NewValue;
107 return self::$RecipientWhitelist;
118 public function Body($NewValue = NULL)
120 if ($NewValue !== NULL) { $this->
Body = $NewValue; }
131 # set the plain-text alternative if a parameter is given 132 if (func_num_args() > 0)
137 return $this->AlternateBody;
147 if ($NewValue !== NULL) { $this->
Subject = $NewValue; }
148 return $this->Subject;
159 public function From($NewAddress = NULL, $NewName = NULL)
161 if ($NewAddress !== NULL)
163 $NewAddress = trim($NewAddress);
164 if ($NewName !== NULL)
166 $NewName = trim($NewName);
167 $this->
From = $NewName.
" <".$NewAddress.
">";
171 $this->
From = $NewAddress;
185 if ($NewValue !== NULL) { self::$DefaultFrom = $NewValue; }
186 return self::$DefaultFrom;
197 public function ReplyTo($NewAddress = NULL, $NewName = NULL)
199 if ($NewAddress !== NULL)
201 $NewAddress = trim($NewAddress);
202 if ($NewName !== NULL)
204 $NewName = trim($NewName);
205 $this->
ReplyTo = $NewName.
" <".$NewAddress.
">";
212 return $this->ReplyTo;
222 public function To($NewValue = NULL)
224 if ($NewValue !== NULL)
226 if (!is_array($NewValue))
228 $this->
To = array($NewValue);
232 $this->
To = $NewValue;
245 public function CC($NewValue = NULL)
247 if ($NewValue !== NULL)
249 if (!is_array($NewValue))
251 $this->
CC = array($NewValue);
255 $this->
CC = $NewValue;
268 public function BCC($NewValue = NULL)
270 if ($NewValue !== NULL)
272 if (!is_array($NewValue))
274 $this->
BCC = array($NewValue);
278 $this->
BCC = $NewValue;
290 # add new headers to list 291 $this->Headers = array_merge($this->Headers, $NewHeaders);
302 # set the plain-text alternative if a parameter is given 303 if (func_num_args() > 0)
308 return $this->CharSet;
318 if (!is_null($NewValue))
320 self::$LineEnding = $NewValue;
323 return self::$LineEnding;
339 $Html, $MaxLineLength=998, $LineEnding=
"\r\n")
341 # the regular expression used to find long lines 342 $LongLineRegExp =
'/[^\r\n]{'.($MaxLineLength+1).
',}/';
344 # find all lines that are too long 345 preg_match_all($LongLineRegExp, $Html, $Matches,
346 PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
348 # no changes are necessary 349 if (!count($Matches))
354 # go backwards so that the HTML can be edited in place without messing 356 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
358 # extract the line text and its offset within the string 359 list($Line, $Offset) = $Matches[0][$i];
361 # first try to get the line under the limit without being too 363 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
364 $WasAggressive =
"No";
366 # if the line is still too long, be more aggressive with replacing 367 # horizontal whitespace 368 if (preg_match($LongLineRegExp, $BetterLine))
370 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
371 $WasAggressive =
"Yes";
374 # tack on an HTML comment stating that the line was wrapped and give 375 # some additional info 376 $BetterLine = $LineEnding.
"<!-- Line was wrapped. Aggressive: " 377 .$WasAggressive.
", Max: ".$MaxLineLength.
", Actual: " 378 .strlen($Line).
" -->".$LineEnding.$BetterLine;
380 # replace the line within the HTML 381 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
396 # the number of \r in the string 397 $NumCR = substr_count($Value,
"\r");
400 if ($LineEnding ==
"\n")
405 # the number of \n in the string 406 $NumLF = substr_count($Value,
"\n");
409 if ($LineEnding ==
"\r")
414 # the number of \r\n in the string 415 $NumCRLF = substr_count($Value,
"\r\n");
417 # CRLF. also check CRLF to make sure CR and LF appear together and in 419 return $NumCR === $NumLF && $NumLF === $NumCRLF;
430 $Text = str_replace(array(
"\r",
"\n"),
"", $Html);
432 # convert HTML breaks to newlines 433 $Text = preg_replace(
'/<br\s*\/?>/',
"\n", $Text);
435 # strip remaining tags 436 $Text = strip_tags($Text);
438 # convert HTML entities to their plain-text equivalents 439 $Text = html_entity_decode($Text);
441 # single quotes aren't always handled 442 $Text = str_replace(
''',
"'", $Text);
444 # remove HTML entities that have no equivalents 445 $Text = preg_replace(
'/&(#[0-9]{1,6}|[a-zA-Z0-9]{1,6});/',
"", $Text);
447 # return the plain text version 461 if ($NewValue !== NULL)
463 self::$DeliveryMethod = $NewValue;
465 return self::$DeliveryMethod;
477 public static function Server($NewValue = NULL)
479 if ($NewValue !== NULL) { self::$Server = $NewValue; }
480 return self::$Server;
488 public static function Port($NewValue = NULL)
490 if ($NewValue !== NULL) { self::$Port = $NewValue; }
501 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
502 return self::$UserName;
512 if ($NewValue !== NULL) { self::$Password = $NewValue; }
513 return self::$Password;
523 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
524 return self::$UseAuthentication;
536 if ($NewSettings !== NULL)
538 $Settings = unserialize($NewSettings);
539 self::$DeliveryMethod = $Settings[
"DeliveryMethod"];
540 self::$Server = $Settings[
"Server"];
541 self::$Port = $Settings[
"Port"];
542 self::$UserName = $Settings[
"UserName"];
543 self::$Password = $Settings[
"Password"];
544 self::$UseAuthentication = $Settings[
"UseAuthentication"];
548 $Settings[
"DeliveryMethod"] = self::$DeliveryMethod;
549 $Settings[
"Server"] = self::$Server;
550 $Settings[
"Port"] = self::$Port;
551 $Settings[
"UserName"] = self::$UserName;
552 $Settings[
"Password"] = self::$Password;
553 $Settings[
"UseAuthentication"] = self::$UseAuthentication;
555 return serialize($Settings);
568 # start out with error list clear 569 self::$DeliverySettingErrorList = array();
571 # test based on delivery method 572 switch (self::$DeliveryMethod)
574 case self::METHOD_PHPMAIL:
575 # always report success 576 $SettingsOkay = TRUE;
579 case self::METHOD_SMTP:
580 # set up PHPMailer for test 581 $PMail =
new PHPMailer(TRUE);
583 $PMail->SMTPAuth = self::$UseAuthentication;
584 $PMail->Host = self::$Server;
585 $PMail->Port = self::$Port;
586 $PMail->Username = self::$UserName;
587 $PMail->Password = self::$Password;
592 $SettingsOkay = $PMail->SmtpConnect();
595 catch (phpmailerException $Except)
597 # translate PHPMailer error message to possibly bad settings 598 switch ($Except->getMessage())
600 case 'SMTP Error: Could not authenticate.':
601 self::$DeliverySettingErrorList = array(
608 case 'SMTP Error: Could not connect to SMTP host.':
609 self::$DeliverySettingErrorList = array(
615 case 'Language string failed to load: tls':
616 self::$DeliverySettingErrorList = array(
"TLS");
620 self::$DeliverySettingErrorList = array(
"UNKNOWN");
624 # make sure failure is reported 625 $SettingsOkay = FALSE;
630 # report result to caller 631 return $SettingsOkay;
640 return self::$DeliverySettingErrorList;
644 # ---- PRIVATE INTERFACE ------------------------------------------------- 646 private $AlternateBody =
"";
647 private $BCC = array();
649 private $CC = array();
652 private $Headers = array();
653 private $ReplyTo =
"";
654 private $Subject =
"";
655 private $To = array();
656 private $Whitelist = array();
658 private static $DefaultFrom =
"";
659 private static $DeliveryMethod = self::METHOD_PHPMAIL;
660 private static $DeliverySettingErrorList = array();
661 private static $LineEnding =
"\r\n";
662 private static $Password =
"";
663 private static $Port = 25;
664 private static $RecipientWhitelist = array();
665 private static $Server;
666 private static $UseAuthentication = FALSE;
667 private static $UserName =
"";
673 private function SendViaPhpMailFunc()
675 # Contrary to the PHP documentation, line endings for PHP's 676 # mail function should be the system native line endings. 678 # see https://bugs.php.net/bug.php?id=15841 for details 680 # Use the system line endings 683 # build basic headers list 684 $From = strlen($this->
From) ? $this->
From : self::$DefaultFrom;
685 $Headers =
"From: ".self::CleanHeaderValue($From).$LE;
686 $Headers .= $this->BuildAddresseeLine(
"Cc", $this->
CC);
687 $Headers .= $this->BuildAddresseeLine(
"Bcc", $this->
BCC);
688 $Headers .=
"Reply-To: ".self::CleanHeaderValue(
691 # add additional headers 692 foreach ($this->Headers as $ExtraHeader)
694 $Headers .= $ExtraHeader.$LE;
697 # build recipient list 700 foreach ($this->
To as $Recipient)
702 $To .= $Separator.$Recipient;
706 # normalize message body line endings 707 $Body = $this->NormalizeLineEndings($this->
Body, $LE);
710 $Result = mail($To, $this->
Subject, $Body, $Headers);
712 # report to caller whether attempt to send succeeded 721 private function SendViaPhpMailerLib()
723 # create and initialize PHPMailer 724 $PMail =
new PHPMailer();
725 $PMail->LE = self::$LineEnding;
726 $PMail->Subject = $this->Subject;
727 $PMail->Body = $this->Body;
728 $PMail->IsHTML(FALSE);
730 # default values for the sender's name and address 732 $Address = $this->From;
734 # if the address contains a name and address, they need to extracted 735 # because PHPMailer requires that they are set as two different 737 if (preg_match(
"/ </", $this->
From))
739 $Pieces = explode(
" ", $this->
From);
740 $Address = array_pop($Pieces);
741 $Address = preg_replace(
"/[<>]+/",
"", $Address);
742 $Name = trim(implode($Pieces,
" "));
746 $PMail->SetFrom($Address, $Name);
749 foreach ($this->
To as $Recipient)
751 $PMail->AddAddress($Recipient);
754 # add any extra header lines 755 foreach ($this->Headers as $ExtraHeader)
757 $PMail->AddCustomHeader($ExtraHeader);
760 # add the charset if it's set 763 $PMail->CharSet = strtolower($this->
CharSet);
766 # add the alternate plain-text body if it's set 767 if ($this->HasAlternateBody())
769 $PMail->AltBody = $this->AlternateBody;
772 # set up SMTP if necessary 773 if (self::$DeliveryMethod == self::METHOD_SMTP)
776 $PMail->SMTPAuth = self::$UseAuthentication;
777 $PMail->Host = self::$Server;
778 $PMail->Port = self::$Port;
779 $PMail->Username = self::$UserName;
780 $PMail->Password = self::$Password;
784 $Result = $PMail->Send();
786 # report to caller whether attempt to send succeeded 794 private function SendViaSmtp()
796 # send via PHPMailer because it's capable of handling SMTP 797 return $this->SendViaPhpMailerLib();
806 private function BuildAddresseeLine($Label, $Recipients)
809 if (count($Recipients))
811 $Line .= $Label.
": ";
813 foreach ($Recipients as $Recipient)
815 $Line .= $Separator.self::CleanHeaderValue($Recipient);
818 $Line .= self::$LineEnding;
827 private function HasAlternateBody()
837 private static function CleanHeaderValue($Value)
839 # (regular expression taken from sanitizeHeaders() function in 841 return preg_replace(
'=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
851 private static function NormalizeLineEndings($Value, $LineEnding)
853 return preg_replace(
'/\r\n|\r|\n/', $LineEnding, $Value);
871 $Html, $Aggressive=FALSE, $LineEnding=
"\r\n")
873 $HtmlLength = strlen($Html);
875 # tags that should have their inner HTML left alone 876 $IgnoredTags = array(
'script',
'style',
'textarea',
'title');
878 # values for determining context 880 $InClosingTag = FALSE;
881 $InIgnoredTag = FALSE;
882 $InAttribute = FALSE;
884 $IgnoredTagName = NULL;
885 $AttributeDelimiter = NULL;
887 # loop through each character of the string 888 for ($i = 0; $i < $HtmlLength; $i++)
893 if ($Char ==
"<" && !$InTag)
896 $InAttribute = FALSE;
897 $AttributeDelimiter = NULL;
899 # do some lookaheads to get the tag name and to see if the tag 901 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
903 # moving into an ignored tag 904 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
906 $InIgnoredTag = TRUE;
907 $IgnoredTagName = $TagName;
914 if ($Char ==
">" && $InTag && !$InAttribute)
916 # moving out of an ignored tag 917 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
919 $InIgnoredTag = FALSE;
920 $IgnoredTagName = NULL;
924 $InClosingTag = FALSE;
925 $InAttribute = FALSE;
927 $AttributeDelimiter = NULL;
932 # attribute delimiter characters 933 if ($Char ==
"'" || $Char ==
'"')
935 # beginning of an attribute 939 $AttributeDelimiter = $Char;
943 # end of the attribute 944 if ($InAttribute && $Char == $AttributeDelimiter)
946 $InAttribute = FALSE;
947 $AttributeDelimiter = NULL;
952 # whitespace inside of a tag but outside of an attribute can be 953 # safely converted to a newline 954 if ($InTag && !$InAttribute && preg_match(
'/\s/', $Char))
956 $Html{$i} = $LineEnding;
960 # whitespace outside of a tag can be safely converted to a newline 961 # when not in one of the ignored tags, but only do so if horizontal 962 # space is at a premium because it can make the resulting HTML 964 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match(
'/\s/', $Char))
966 $Html{$i} = $LineEnding;
984 $HtmlLength = strlen($Html);
986 # default return values 987 $InClosingTag = FALSE;
990 # if at the end of the string and lookaheads aren't possible 991 if ($TagBegin + 1 >= $HtmlLength)
993 return array($InClosingTag, $TagName);
996 # do a lookahead for whether it's a closing tag 997 if ($Html{$TagBegin+1} ==
"/")
999 $InClosingTag = TRUE;
1002 # determine whether to offset by one or two to get the tag name 1003 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
1005 # do a lookahead for the tag name 1006 for ($i = $TagStart; $i < $HtmlLength; $i++)
1010 # stop getting the tag name if whitespace is found and something is 1011 # available for the tag name 1012 if (strlen($TagName) && preg_match(
'/[\r\n\s]/', $Char))
1017 # stop getting the tag name if the character is > 1027 if (substr($TagName, 0, 3) ==
"!--")
1029 return array($InClosingTag,
"!--");
1032 # remove characters that aren't part of a valid tag name 1033 $TagName = preg_replace(
'/[^a-zA-Z0-9]/',
'', $TagName);
1035 return array($InClosingTag, $TagName);
From($NewAddress=NULL, $NewName=NULL)
Get/set message sender.
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...
AddHeaders($NewHeaders)
Specify additional message headers to be included.
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...