5 # Part of the ScoutLib application support library
6 # Copyright 2012-2013 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
16 # ---- PUBLIC INTERFACE --------------------------------------------------
30 if (count(self::$RecipientWhitelist))
32 # save recipient list and then pare it down based on whitelist
35 foreach ($this->
To as $To)
37 foreach (self::$RecipientWhitelist as $White)
39 $White = trim($White);
40 if ($White[0] != substr($White, 0, -1))
42 $White =
"/".$White.
"/";
44 if (preg_match($White, $To))
54 # if there are recipients
58 switch (self::$DeliveryMethod)
60 case self::METHOD_PHPMAIL:
61 # use PHPMailer to send multipart alternative messages because
62 # they can be tricky to construct properly
63 if ($this->HasAlternateBody())
65 $Result = $this->SendViaPhpMailerLib();
68 # otherwise, just use the built-in mail() function
71 $Result = $this->SendViaPhpMailFunc();
75 case self::METHOD_SMTP:
76 $Result = $this->SendViaSmtp();
81 # if recipient list saved
84 # restore recipient list
88 # report to caller whether message was sent
102 if ($NewValue !== NULL)
104 self::$RecipientWhitelist = $NewValue;
106 return self::$RecipientWhitelist;
117 function Body($NewValue = NULL)
119 if ($NewValue !== NULL) { $this->
Body = $NewValue; }
130 # set the plain-text alternative if a parameter is given
131 if (func_num_args() > 0)
136 return $this->AlternateBody;
146 if ($NewValue !== NULL) { $this->
Subject = $NewValue; }
147 return $this->Subject;
158 function From($NewAddress = NULL, $NewName = NULL)
160 if ($NewAddress !== NULL)
162 $NewAddress = trim($NewAddress);
163 if ($NewName !== NULL)
165 $NewName = trim($NewName);
166 $this->
From = $NewName.
" <".$NewAddress.
">";
170 $this->
From = $NewAddress;
184 if ($NewValue !== NULL) { self::$DefaultFrom = $NewValue; }
185 return self::$DefaultFrom;
196 function ReplyTo($NewAddress = NULL, $NewName = NULL)
198 if ($NewAddress !== NULL)
200 $NewAddress = trim($NewAddress);
201 if ($NewName !== NULL)
203 $NewName = trim($NewName);
204 $this->
ReplyTo = $NewName.
" <".$NewAddress.
">";
211 return $this->ReplyTo;
221 function To($NewValue = NULL)
223 if ($NewValue !== NULL)
225 if (!is_array($NewValue))
227 $this->
To = array($NewValue);
231 $this->
To = $NewValue;
244 function CC($NewValue = NULL)
246 if ($NewValue !== NULL)
248 if (!is_array($NewValue))
250 $this->
CC = array($NewValue);
254 $this->
CC = $NewValue;
267 function BCC($NewValue = NULL)
269 if ($NewValue !== NULL)
271 if (!is_array($NewValue))
273 $this->
BCC = array($NewValue);
277 $this->
BCC = $NewValue;
289 # add new headers to list
290 $this->Headers = array_merge($this->Headers, $NewHeaders);
301 # set the plain-text alternative if a parameter is given
302 if (func_num_args() > 0)
307 return $this->CharSet;
317 if (!is_null($NewValue))
319 self::$LineEnding = $NewValue;
322 return self::$LineEnding;
338 $Html, $MaxLineLength=998, $LineEnding=
"\r\n")
340 # the regular expression used to find long lines
341 $LongLineRegExp =
'/[^\r\n]{'.($MaxLineLength+1).
',}/';
343 # find all lines that are too long
344 preg_match_all($LongLineRegExp, $Html, $Matches,
345 PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
347 # no changes are necessary
348 if (!count($Matches))
353 # go backwards so that the HTML can be edited in place without messing
355 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
357 # extract the line text and its offset within the string
358 list($Line, $Offset) = $Matches[0][$i];
360 # first try to get the line under the limit without being too
362 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
363 $WasAggressive =
"No";
365 # if the line is still too long, be more aggressive with replacing
366 # horizontal whitespace
367 if (preg_match($LongLineRegExp, $BetterLine))
369 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
370 $WasAggressive =
"Yes";
373 # tack on an HTML comment stating that the line was wrapped and give
374 # some additional info
375 $BetterLine = $LineEnding.
"<!-- Line was wrapped. Aggressive: "
376 .$WasAggressive.
", Max: ".$MaxLineLength.
", Actual: "
377 .strlen($Line).
" -->".$LineEnding.$BetterLine;
379 # replace the line within the HTML
380 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
395 # the number of \r in the string
396 $NumCR = substr_count($Value,
"\r");
399 if ($LineEnding ==
"\n")
404 # the number of \n in the string
405 $NumLF = substr_count($Value,
"\n");
408 if ($LineEnding ==
"\r")
413 # the number of \r\n in the string
414 $NumCRLF = substr_count($Value,
"\r\n");
416 # CRLF. also check CRLF to make sure CR and LF appear together and in
418 return $NumCR === $NumLF && $NumLF === $NumCRLF;
429 $Text = str_replace(array(
"\r",
"\n"),
"", $Html);
431 # convert HTML breaks to newlines
432 $Text = preg_replace(
'/<br\s*\/?>/',
"\n", $Text);
434 # strip remaining tags
435 $Text = strip_tags($Text);
437 # convert HTML entities to their plain-text equivalents
438 $Text = html_entity_decode($Text);
440 # single quotes aren't always handled
441 $Text = str_replace(
''',
"'", $Text);
443 # remove HTML entities that have no equivalents
444 $Text = preg_replace(
'/&(#[0-9]{1,6}|[a-zA-Z0-9]{1,6});/',
"", $Text);
446 # return the plain text version
460 if ($NewValue !== NULL)
462 self::$DeliveryMethod = $NewValue;
464 return self::$DeliveryMethod;
478 if ($NewValue !== NULL) { self::$Server = $NewValue; }
479 return self::$Server;
487 static function Port($NewValue = NULL)
489 if ($NewValue !== NULL) { self::$Port = $NewValue; }
500 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
501 return self::$UserName;
511 if ($NewValue !== NULL) { self::$Password = $NewValue; }
512 return self::$Password;
522 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
523 return self::$UseAuthentication;
535 if ($NewSettings !== NULL)
537 $Settings = unserialize($NewSettings);
538 self::$DeliveryMethod = $Settings[
"DeliveryMethod"];
539 self::$Server = $Settings[
"Server"];
540 self::$Port = $Settings[
"Port"];
541 self::$UserName = $Settings[
"UserName"];
542 self::$Password = $Settings[
"Password"];
543 self::$UseAuthentication = $Settings[
"UseAuthentication"];
547 $Settings[
"DeliveryMethod"] = self::$DeliveryMethod;
548 $Settings[
"Server"] = self::$Server;
549 $Settings[
"Port"] = self::$Port;
550 $Settings[
"UserName"] = self::$UserName;
551 $Settings[
"Password"] = self::$Password;
552 $Settings[
"UseAuthentication"] = self::$UseAuthentication;
554 return serialize($Settings);
567 # start out with error list clear
568 self::$DeliverySettingErrorList = array();
570 # test based on delivery method
571 switch (self::$DeliveryMethod)
573 case self::METHOD_PHPMAIL:
574 # always report success
575 $SettingsOkay = TRUE;
578 case self::METHOD_SMTP:
579 # set up PHPMailer for test
580 $PMail =
new PHPMailer(TRUE);
582 $PMail->SMTPAuth = self::$UseAuthentication;
583 $PMail->Host = self::$Server;
584 $PMail->Port = self::$Port;
585 $PMail->Username = self::$UserName;
586 $PMail->Password = self::$Password;
591 $SettingsOkay = $PMail->SmtpConnect();
594 catch (phpmailerException $Except)
596 # translate PHPMailer error message to possibly bad settings
597 switch ($Except->getMessage())
599 case 'SMTP Error: Could not authenticate.':
600 self::$DeliverySettingErrorList = array(
607 case 'SMTP Error: Could not connect to SMTP host.':
608 self::$DeliverySettingErrorList = array(
614 case 'Language string failed to load: tls':
615 self::$DeliverySettingErrorList = array(
"TLS");
619 self::$DeliverySettingErrorList = array(
"UNKNOWN");
623 # make sure failure is reported
624 $SettingsOkay = FALSE;
629 # report result to caller
630 return $SettingsOkay;
639 return self::$DeliverySettingErrorList;
643 # ---- PRIVATE INTERFACE -------------------------------------------------
645 private $AlternateBody =
"";
646 private $BCC = array();
648 private $CC = array();
651 private $Headers = array();
652 private $ReplyTo =
"";
653 private $Subject =
"";
654 private $To = array();
655 private $Whitelist = array();
657 private static $DefaultFrom =
"";
658 private static $DeliveryMethod = self::METHOD_PHPMAIL;
659 private static $DeliverySettingErrorList = array();
660 private static $LineEnding =
"\r\n";
661 private static $Password =
"";
662 private static $Port = 25;
663 private static $RecipientWhitelist = array();
664 private static $Server;
665 private static $UseAuthentication = FALSE;
666 private static $UserName =
"";
672 private function SendViaPhpMailFunc()
674 # Contrary to the PHP documentation, line endings for PHP's
675 # mail function should be the system native line endings.
677 # see https://bugs.php.net/bug.php?id=15841 for details
679 # Use the system line endings
682 # build basic headers list
683 $From = strlen($this->
From) ? $this->
From : self::$DefaultFrom;
684 $Headers =
"From: ".self::CleanHeaderValue($From).$LE;
685 $Headers .= $this->BuildAddresseeLine(
"Cc", $this->
CC);
686 $Headers .= $this->BuildAddresseeLine(
"Bcc", $this->
BCC);
687 $Headers .=
"Reply-To: ".self::CleanHeaderValue(
690 # add additional headers
691 foreach ($this->Headers as $ExtraHeader)
693 $Headers .= $ExtraHeader.$LE;
696 # build recipient list
699 foreach ($this->
To as $Recipient)
701 $To .= $Separator.$Recipient;
705 # normalize message body line endings
706 $Body = $this->NormalizeLineEndings($this->
Body, $LE);
709 $Result = mail($To, $this->
Subject, $Body, $Headers);
711 # report to caller whether attempt to send succeeded
720 private function SendViaPhpMailerLib()
722 # create and initialize PHPMailer
723 $PMail =
new PHPMailer();
724 $PMail->LE = self::$LineEnding;
725 $PMail->Subject = $this->Subject;
726 $PMail->Body = $this->Body;
727 $PMail->IsHTML(FALSE);
729 # default values for the sender's name and address
731 $Address = $this->From;
733 # if the address contains a name and address, they need to extracted
734 # because PHPMailer requires that they are set as two different
736 if (preg_match(
"/ </", $this->From))
738 $Pieces = explode(
" ", $this->From);
739 $Address = array_pop($Pieces);
740 $Address = preg_replace(
"/[<>]+/",
"", $Address);
741 $Name = trim(implode($Pieces,
" "));
745 $PMail->SetFrom($Address, $Name);
748 foreach ($this->
To as $Recipient)
750 $PMail->AddAddress($Recipient);
753 # add any extra header lines
754 foreach ($this->Headers as $ExtraHeader)
756 $PMail->AddCustomHeader($ExtraHeader);
759 # add the charset if it's set
762 $PMail->CharSet = strtolower($this->
CharSet);
765 # add the alternate plain-text body if it's set
766 if ($this->HasAlternateBody())
768 $PMail->AltBody = $this->AlternateBody;
771 # set up SMTP if necessary
772 if (self::$DeliveryMethod == self::METHOD_SMTP)
775 $PMail->SMTPAuth = self::$UseAuthentication;
776 $PMail->Host = self::$Server;
777 $PMail->Port = self::$Port;
778 $PMail->Username = self::$UserName;
779 $PMail->Password = self::$Password;
783 $Result = $PMail->Send();
785 # report to caller whether attempt to send succeeded
793 private function SendViaSmtp()
795 # send via PHPMailer because it's capable of handling SMTP
796 return $this->SendViaPhpMailerLib();
805 private function BuildAddresseeLine($Label, $Recipients)
808 if (count($Recipients))
810 $Line .= $Label.
": ";
812 foreach ($Recipients as $Recipient)
814 $Line .= $Separator.self::CleanHeaderValue($Recipient);
817 $Line .= self::$LineEnding;
826 private function HasAlternateBody()
836 private static function CleanHeaderValue($Value)
838 # (regular expression taken from sanitizeHeaders() function in
840 return preg_replace(
'=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
850 private static function NormalizeLineEndings($Value, $LineEnding)
852 return preg_replace(
'/\r\n|\r|\n/', $LineEnding, $Value);
870 $Html, $Aggressive=FALSE, $LineEnding=
"\r\n")
872 $HtmlLength = strlen($Html);
874 # tags that should have their inner HTML left alone
875 $IgnoredTags = array(
'script',
'style',
'textarea',
'title');
877 # values for determining context
879 $InClosingTag = FALSE;
880 $InIgnoredTag = FALSE;
881 $InAttribute = FALSE;
883 $IgnoredTagName = NULL;
884 $AttributeDelimiter = NULL;
886 # loop through each character of the string
887 for ($i = 0; $i < $HtmlLength; $i++)
892 if ($Char ==
"<" && !$InTag)
895 $InAttribute = FALSE;
896 $AttributeDelimiter = NULL;
898 # do some lookaheads to get the tag name and to see if the tag
900 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
902 # moving into an ignored tag
903 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
905 $InIgnoredTag = TRUE;
906 $IgnoredTagName = $TagName;
913 if ($Char ==
">" && $InTag && !$InAttribute)
915 # moving out of an ignored tag
916 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
918 $InIgnoredTag = FALSE;
919 $IgnoredTagName = NULL;
923 $InClosingTag = FALSE;
924 $InAttribute = FALSE;
926 $AttributeDelimiter = NULL;
931 # attribute delimiter characters
932 if ($Char ==
"'" || $Char ==
'"')
934 # beginning of an attribute
938 $AttributeDelimiter = $Char;
942 # end of the attribute
943 if ($InAttribute && $Char == $AttributeDelimiter)
945 $InAttribute = FALSE;
946 $AttributeDelimiter = NULL;
951 # whitespace inside of a tag but outside of an attribute can be
952 # safely converted to a newline
953 if ($InTag && !$InAttribute && preg_match(
'/\s/', $Char))
955 $Html{$i} = $LineEnding;
959 # whitespace outside of a tag can be safely converted to a newline
960 # when not in one of the ignored tags, but only do so if horizontal
961 # space is at a premium because it can make the resulting HTML
963 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match(
'/\s/', $Char))
965 $Html{$i} = $LineEnding;
983 $HtmlLength = strlen($Html);
985 # default return values
986 $InClosingTag = FALSE;
989 # if at the end of the string and lookaheads aren't possible
990 if ($TagBegin + 1 >= $HtmlLength)
992 return array($InClosingTag, $TagName);
995 # do a lookahead for whether it's a closing tag
996 if ($Html{$TagBegin+1} ==
"/")
998 $InClosingTag = TRUE;
1001 # determine whether to offset by one or two to get the tag name
1002 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
1004 # do a lookahead for the tag name
1005 for ($i = $TagStart; $i < $HtmlLength; $i++)
1009 # stop getting the tag name if whitespace is found and something is
1010 # available for the tag name
1011 if (strlen($TagName) && preg_match(
'/[\r\n\s]/', $Char))
1016 # stop getting the tag name if the character is >
1026 if (substr($TagName, 0, 3) ==
"!--")
1028 return array($InClosingTag,
"!--");
1031 # remove characters that aren't part of a valid tag name
1032 $TagName = preg_replace(
'/[^a-zA-Z0-9]/',
'', $TagName);
1034 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)
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...