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 --------------------------------------------------
27 switch (self::$DeliveryMethod)
29 case self::METHOD_PHPMAIL:
30 # use PHPMailer to send multipart alternative messages because
31 # they can be tricky to construct properly
32 if ($this->HasAlternateBody())
34 $Result = $this->SendViaPhpMailerLib();
37 # otherwise, just use the built-in mail() function
40 $Result = $this->SendViaPhpMailFunc();
44 case self::METHOD_SMTP:
45 $Result = $this->SendViaSmtp();
59 function Body($NewValue = NULL)
61 if ($NewValue !== NULL) { $this->
Body = $NewValue; }
72 # set the plain-text alternative if a parameter is given
73 if (func_num_args() > 0)
78 return $this->AlternateBody;
88 if ($NewValue !== NULL) { $this->
Subject = $NewValue; }
89 return $this->Subject;
100 function From($NewAddress = NULL, $NewName = NULL)
102 if ($NewAddress !== NULL)
104 $NewAddress = trim($NewAddress);
105 if ($NewName !== NULL)
107 $NewName = trim($NewName);
108 $this->
From = $NewName.
" <".$NewAddress.
">";
112 $this->
From = $NewAddress;
126 if ($NewValue !== NULL) { self::$DefaultFrom = $NewValue; }
127 return self::$DefaultFrom;
138 function ReplyTo($NewAddress = NULL, $NewName = NULL)
140 if ($NewAddress !== NULL)
142 $NewAddress = trim($NewAddress);
143 if ($NewName !== NULL)
145 $NewName = trim($NewName);
146 $this->
ReplyTo = $NewName.
" <".$NewAddress.
">";
153 return $this->ReplyTo;
163 function To($NewValue = NULL)
165 if ($NewValue !== NULL)
167 if (!is_array($NewValue))
169 $this->
To = array($NewValue);
173 $this->
To = $NewValue;
186 function CC($NewValue = NULL)
188 if ($NewValue !== NULL)
190 if (!is_array($NewValue))
192 $this->
CC = array($NewValue);
196 $this->
CC = $NewValue;
209 function BCC($NewValue = NULL)
211 if ($NewValue !== NULL)
213 if (!is_array($NewValue))
215 $this->
BCC = array($NewValue);
219 $this->
BCC = $NewValue;
231 # add new headers to list
232 $this->Headers = array_merge($this->Headers, $NewHeaders);
243 # set the plain-text alternative if a parameter is given
244 if (func_num_args() > 0)
249 return $this->CharSet;
259 if (!is_null($NewValue))
261 self::$LineEnding = $NewValue;
264 return self::$LineEnding;
281 # the regular expression used to find long lines
282 $LongLineRegExp =
'/[^\r\n]{'.($MaxLineLength+1).
',}/';
284 # find all lines that are too long
285 preg_match_all($LongLineRegExp, $Html, $Matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
287 # no changes are necessary
288 if (!count($Matches))
293 # go backwards so that the HTML can be edited in place without messing
295 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
297 # extract the line text and its offset within the string
298 list($Line, $Offset) = $Matches[0][$i];
300 # first try to get the line under the limit without being too
302 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
303 $WasAggressive =
"No";
305 # if the line is still too long, be more aggressive with replacing
306 # horizontal whitespace
307 if (preg_match($LongLineRegExp, $BetterLine))
309 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
310 $WasAggressive =
"Yes";
313 # tack on an HTML comment stating that the line was wrapped and give
314 # some additional info
315 $BetterLine = $LineEnding.
"<!-- Line was wrapped. Aggressive: "
316 .$WasAggressive.
", Max: ".$MaxLineLength.
", Actual: "
317 .strlen($Line).
" -->".$LineEnding.$BetterLine;
319 # replace the line within the HTML
320 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
335 # the number of \r in the string
336 $NumCR = substr_count($Value,
"\r");
339 if ($LineEnding ==
"\n")
344 # the number of \n in the string
345 $NumLF = substr_count($Value,
"\n");
348 if ($LineEnding ==
"\r")
353 # the number of \r\n in the string
354 $NumCRLF = substr_count($Value,
"\r\n");
356 # CRLF. also check CRLF to make sure CR and LF appear together and in
358 return $NumCR === $NumLF && $NumLF === $NumCRLF;
369 $Text = str_replace(array(
"\r",
"\n"),
"", $Html);
371 # convert HTML breaks to newlines
372 $Text = preg_replace(
'/<br\s*\/?>/',
"\n", $Text);
374 # strip remaining tags
375 $Text = strip_tags($Text);
377 # convert HTML entities to their plain-text equivalents
378 $Text = html_entity_decode($Text);
380 # single quotes aren't always handled
381 $Text = str_replace(
''',
"'", $Text);
383 # remove HTML entities that have no equivalents
384 $Text = preg_replace(
'/&(#[0-9]{1,6}|[a-zA-Z0-9]{1,6});/',
"", $Text);
386 # return the plain text version
400 if ($NewValue !== NULL)
402 self::$DeliveryMethod = $NewValue;
404 return self::$DeliveryMethod;
418 if ($NewValue !== NULL) { self::$Server = $NewValue; }
419 return self::$Server;
427 static function Port($NewValue = NULL)
429 if ($NewValue !== NULL) { self::$Port = $NewValue; }
440 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
441 return self::$UserName;
451 if ($NewValue !== NULL) { self::$Password = $NewValue; }
452 return self::$Password;
462 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
463 return self::$UseAuthentication;
475 if ($NewSettings !== NULL)
477 $Settings = unserialize($NewSettings);
478 self::$DeliveryMethod = $Settings[
"DeliveryMethod"];
479 self::$Server = $Settings[
"Server"];
480 self::$Port = $Settings[
"Port"];
481 self::$UserName = $Settings[
"UserName"];
482 self::$Password = $Settings[
"Password"];
483 self::$UseAuthentication = $Settings[
"UseAuthentication"];
487 $Settings[
"DeliveryMethod"] = self::$DeliveryMethod;
488 $Settings[
"Server"] = self::$Server;
489 $Settings[
"Port"] = self::$Port;
490 $Settings[
"UserName"] = self::$UserName;
491 $Settings[
"Password"] = self::$Password;
492 $Settings[
"UseAuthentication"] = self::$UseAuthentication;
494 return serialize($Settings);
507 # start out with error list clear
508 self::$DeliverySettingErrorList = array();
510 # test based on delivery method
511 switch (self::$DeliveryMethod)
513 case self::METHOD_PHPMAIL:
514 # always report success
515 $SettingsOkay = TRUE;
518 case self::METHOD_SMTP:
519 # set up PHPMailer for test
520 $PMail =
new PHPMailer(TRUE);
522 $PMail->SMTPAuth = self::$UseAuthentication;
523 $PMail->Host = self::$Server;
524 $PMail->Port = self::$Port;
525 $PMail->Username = self::$UserName;
526 $PMail->Password = self::$Password;
531 $SettingsOkay = $PMail->SmtpConnect();
534 catch (phpmailerException $Except)
536 # translate PHPMailer error message to possibly bad settings
537 switch ($Except->getMessage())
539 case 'SMTP Error: Could not authenticate.':
540 self::$DeliverySettingErrorList = array(
547 case 'SMTP Error: Could not connect to SMTP host.':
548 self::$DeliverySettingErrorList = array(
554 case 'Language string failed to load: tls':
555 self::$DeliverySettingErrorList = array(
"TLS");
559 self::$DeliverySettingErrorList = array(
"UNKNOWN");
563 # make sure failure is reported
564 $SettingsOkay = FALSE;
569 # report result to caller
570 return $SettingsOkay;
579 return self::$DeliverySettingErrorList;
583 # ---- PRIVATE INTERFACE -------------------------------------------------
586 private $ReplyTo =
"";
587 private $To = array();
588 private $CC = array();
589 private $BCC = array();
591 private $AlternateBody =
"";
592 private $Subject =
"";
593 private $Headers = array();
595 private static $LineEnding =
"\r\n";
596 private static $DefaultFrom =
"";
598 private static $DeliveryMethod = self::METHOD_PHPMAIL;
599 private static $DeliverySettingErrorList = array();
600 private static $Server;
601 private static $Port = 25;
602 private static $UserName =
"";
603 private static $Password =
"";
604 private static $UseAuthentication = FALSE;
610 private function SendViaPhpMailFunc()
612 # Contrary to the PHP documentation, line endings for PHP's
613 # mail function should be the system native line endings.
615 # see https://bugs.php.net/bug.php?id=15841 for details
617 # Use the system line endings
620 # build basic headers list
621 $From = strlen($this->
From) ? $this->
From : self::$DefaultFrom;
622 $Headers =
"From: ".self::CleanHeaderValue($From).$LE;
623 $Headers .= $this->BuildAddresseeLine(
"Cc", $this->
CC);
624 $Headers .= $this->BuildAddresseeLine(
"Bcc", $this->
BCC);
625 $Headers .=
"Reply-To: ".self::CleanHeaderValue(
628 # add additional headers
629 foreach ($this->Headers as $ExtraHeader)
631 $Headers .= $ExtraHeader.$LE;
634 # build recipient list
637 foreach ($this->
To as $Recipient)
639 $To .= $Separator.$Recipient;
643 # normalize message body line endings
644 $Body = $this->NormalizeLineEndings($this->
Body, $LE);
647 $Result = mail($To, $this->
Subject, $Body, $Headers);
649 # report to caller whether attempt to send succeeded
658 private function SendViaPhpMailerLib()
660 # create and initialize PHPMailer
661 $PMail =
new PHPMailer();
662 $PMail->LE = self::$LineEnding;
663 $PMail->Subject = $this->Subject;
664 $PMail->Body = $this->Body;
665 $PMail->IsHTML(FALSE);
667 # default values for the sender's name and address
669 $Address = $this->From;
671 # if the address contains a name and address, they need to extracted
672 # because PHPMailer requires that they are set as two different
674 if (preg_match(
"/ </", $this->From))
676 $Pieces = explode(
" ", $this->From);
677 $Address = array_pop($Pieces);
678 $Address = preg_replace(
"/[<>]+/",
"", $Address);
679 $Name = trim(implode($Pieces,
" "));
683 $PMail->SetFrom($Address, $Name);
686 foreach ($this->
To as $Recipient)
688 $PMail->AddAddress($Recipient);
691 # add any extra header lines
692 foreach ($this->Headers as $ExtraHeader)
694 $PMail->AddCustomHeader($ExtraHeader);
697 # add the charset if it's set
700 $PMail->CharSet = strtolower($this->
CharSet);
703 # add the alternate plain-text body if it's set
704 if ($this->HasAlternateBody())
706 $PMail->AltBody = $this->AlternateBody;
709 # set up SMTP if necessary
710 if (self::$DeliveryMethod == self::METHOD_SMTP)
713 $PMail->SMTPAuth = self::$UseAuthentication;
714 $PMail->Host = self::$Server;
715 $PMail->Port = self::$Port;
716 $PMail->Username = self::$UserName;
717 $PMail->Password = self::$Password;
721 $Result = $PMail->Send();
723 # report to caller whether attempt to send succeeded
731 private function SendViaSmtp()
733 # send via PHPMailer because it's capable of handling SMTP
734 return $this->SendViaPhpMailerLib();
743 private function BuildAddresseeLine($Label, $Recipients)
746 if (count($Recipients))
748 $Line .= $Label.
": ";
750 foreach ($Recipients as $Recipient)
752 $Line .= $Separator.self::CleanHeaderValue($Recipient);
755 $Line .= self::$LineEnding;
764 private function HasAlternateBody()
774 private static function CleanHeaderValue($Value)
776 # (regular expression taken from sanitizeHeaders() function in
778 return preg_replace(
'=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
788 private static function NormalizeLineEndings($Value, $LineEnding)
790 return preg_replace(
'/\r\n|\r|\n/', $LineEnding, $Value);
809 $HtmlLength = strlen($Html);
811 # tags that should have their inner HTML left alone
812 $IgnoredTags = array(
'script',
'style',
'textarea',
'title');
814 # values for determining context
816 $InClosingTag = FALSE;
817 $InIgnoredTag = FALSE;
818 $InAttribute = FALSE;
820 $IgnoredTagName = NULL;
821 $AttributeDelimiter = NULL;
823 # loop through each character of the string
824 for ($i = 0; $i < $HtmlLength; $i++)
829 if ($Char ==
"<" && !$InTag)
832 $InAttribute = FALSE;
833 $AttributeDelimiter = NULL;
835 # do some lookaheads to get the tag name and to see if the tag
837 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
839 # moving into an ignored tag
840 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
842 $InIgnoredTag = TRUE;
843 $IgnoredTagName = $TagName;
850 if ($Char ==
">" && $InTag && !$InAttribute)
852 # moving out of an ignored tag
853 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
855 $InIgnoredTag = FALSE;
856 $IgnoredTagName = NULL;
860 $InClosingTag = FALSE;
861 $InAttribute = FALSE;
863 $AttributeDelimiter = NULL;
868 # attribute delimiter characters
869 if ($Char ==
"'" || $Char ==
'"')
871 # beginning of an attribute
875 $AttributeDelimiter = $Char;
879 # end of the attribute
880 if ($InAttribute && $Char == $AttributeDelimiter)
882 $InAttribute = FALSE;
883 $AttributeDelimiter = NULL;
888 # whitespace inside of a tag but outside of an attribute can be
889 # safely converted to a newline
890 if ($InTag && !$InAttribute && preg_match(
'/\s/', $Char))
892 $Html{$i} = $LineEnding;
896 # whitespace outside of a tag can be safely converted to a newline
897 # when not in one of the ignored tags, but only do so if horizontal
898 # space is at a premium because it can make the resulting HTML
900 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match(
'/\s/', $Char))
902 $Html{$i} = $LineEnding;
920 $HtmlLength = strlen($Html);
922 # default return values
923 $InClosingTag = FALSE;
926 # if at the end of the string and lookaheads aren't possible
927 if ($TagBegin + 1 >= $HtmlLength)
929 return array($InClosingTag, $TagName);
932 # do a lookahead for whether it's a closing tag
933 if ($Html{$TagBegin+1} ==
"/")
935 $InClosingTag = TRUE;
938 # determine whether to offset by one or two to get the tag name
939 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
941 # do a lookahead for the tag name
942 for ($i = $TagStart; $i < $HtmlLength; $i++)
946 # stop getting the tag name if whitespace is found and something is
947 # available for the tag name
948 if (strlen($TagName) && preg_match(
'/[\r\n\s]/', $Char))
953 # stop getting the tag name if the character is >
963 if (substr($TagName, 0, 3) ==
"!--")
965 return array($InClosingTag,
"!--");
968 # remove characters that aren't part of a valid tag name
969 $TagName = preg_replace(
'/[^a-zA-Z0-9]/',
'', $TagName);
971 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 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...