6 # Part of the ScoutLib application support library
7 # Copyright 2012 Edward Almasy and Internet Scout
8 # http://scout.wisc.edu
17 # ---- PUBLIC INTERFACE --------------------------------------------------
36 switch (self::$DeliveryMethod)
38 case self::METHOD_PHPMAIL:
39 # use PHPMailer to send multipart alternative messages because
40 # they can be tricky to construct properly
41 if ($this->HasAlternateBody())
43 $Result = $this->SendViaPhpMailer();
46 # otherwise, just use the built-in mail() function
49 $Result = $this->SendViaPhpMail();
53 case self::METHOD_SMTP:
54 $Result = $this->SendViaSmtp();
68 function Body($NewValue = NULL)
70 if ($NewValue !== NULL) { $this->
Body = $NewValue; }
81 # set the plain-text alternative if a parameter is given
82 if (func_num_args() > 0)
87 return $this->AlternateBody;
97 if ($NewValue !== NULL) { $this->
Subject = $NewValue; }
98 return $this->Subject;
109 function From($NewAddress = NULL, $NewName = NULL)
111 if ($NewAddress !== NULL)
113 $NewAddress = trim($NewAddress);
114 if ($NewName !== NULL)
116 $NewName = trim($NewName);
117 $this->
From = $NewName.
" <".$NewAddress.
">";
121 $this->
From = $NewAddress;
135 function ReplyTo($NewAddress = NULL, $NewName = NULL)
137 if ($NewAddress !== NULL)
139 $NewAddress = trim($NewAddress);
140 if ($NewName !== NULL)
142 $NewName = trim($NewName);
143 $this->
ReplyTo = $NewName.
" <".$NewAddress.
">";
150 return $this->ReplyTo;
160 function To($NewValue = NULL)
162 if ($NewValue !== NULL)
164 if (!is_array($NewValue))
166 $this->
To = array($NewValue);
170 $this->
To = $NewValue;
183 function CC($NewValue = NULL)
185 if ($NewValue !== NULL)
187 if (!is_array($NewValue))
189 $this->
CC = array($NewValue);
193 $this->
CC = $NewValue;
206 function BCC($NewValue = NULL)
208 if ($NewValue !== NULL)
210 if (!is_array($NewValue))
212 $this->
BCC = array($NewValue);
216 $this->
BCC = $NewValue;
228 # add new headers to list
229 $this->Headers = array_merge($this->Headers, $NewHeaders);
240 # set the plain-text alternative if a parameter is given
241 if (func_num_args() > 0)
246 return $this->CharSet;
256 if (!is_null($NewValue))
258 self::$LineEnding = $NewValue;
261 return self::$LineEnding;
278 # the regular expression used to find long lines
279 $LongLineRegExp =
'/[^\r\n]{'.($MaxLineLength+1).
',}/';
281 # find all lines that are too long
282 preg_match_all($LongLineRegExp, $Html, $Matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
284 # no changes are necessary
285 if (!count($Matches))
290 # go backwards so that the HTML can be edited in place without messing
292 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
294 # extract the line text and its offset within the string
295 list($Line, $Offset) = $Matches[0][$i];
297 # first try to get the line under the limit without being too
299 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
300 $WasAggressive =
"No";
302 # if the line is still too long, be more aggressive with replacing
303 # horizontal whitespace
304 if (preg_match($LongLineRegExp, $BetterLine))
306 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
307 $WasAggressive =
"Yes";
310 # tack on an HTML comment stating that the line was wrapped and give
311 # some additional info
312 $BetterLine = $LineEnding.
"<!-- Line was wrapped. Aggressive: "
313 .$WasAggressive.
", Max: ".$MaxLineLength.
", Actual: "
314 .strlen($Line).
" -->".$LineEnding.$BetterLine;
316 # replace the line within the HTML
317 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
332 # the number of \r in the string
333 $NumCR = substr_count($Value,
"\r");
336 if ($LineEnding ==
"\n")
341 # the number of \n in the string
342 $NumLF = substr_count($Value,
"\n");
345 if ($LineEnding ==
"\r")
350 # the number of \r\n in the string
351 $NumCRLF = substr_count($Value,
"\r\n");
353 # CRLF. also check CRLF to make sure CR and LF appear together and in
355 return $NumCR === $NumLF && $NumLF === $NumCRLF;
368 if ($NewValue !== NULL)
370 self::$DeliveryMethod = $NewValue;
372 return self::$DeliveryMethod;
384 if ($NewValue !== NULL) { self::$Server = $NewValue; }
385 return self::$Server;
393 static function Port($NewValue = NULL)
395 if ($NewValue !== NULL) { self::$Port = $NewValue; }
406 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
407 return self::$UserName;
417 if ($NewValue !== NULL) { self::$Password = $NewValue; }
418 return self::$Password;
428 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
429 return self::$UseAuthentication;
441 if ($NewSettings !== NULL)
443 $Settings = unserialize($NewSettings);
444 self::$DeliveryMethod = $Settings[
"DeliveryMethod"];
445 self::$Server = $Settings[
"Server"];
446 self::$Port = $Settings[
"Port"];
447 self::$UserName = $Settings[
"UserName"];
448 self::$Password = $Settings[
"Password"];
449 self::$UseAuthentication = $Settings[
"UseAuthentication"];
453 $Settings[
"DeliveryMethod"] = self::$DeliveryMethod;
454 $Settings[
"Server"] = self::$Server;
455 $Settings[
"Port"] = self::$Port;
456 $Settings[
"UserName"] = self::$UserName;
457 $Settings[
"Password"] = self::$Password;
458 $Settings[
"UseAuthentication"] = self::$UseAuthentication;
460 return serialize($Settings);
473 # start out with error list clear
474 self::$DeliverySettingErrorList = array();
476 # test based on delivery method
477 switch (self::$DeliveryMethod)
479 case self::METHOD_PHPMAIL:
480 # always report success
481 $SettingsOkay = TRUE;
484 case self::METHOD_SMTP:
485 # set up PHPMailer for test
488 $PMail->SMTPAuth = self::$UseAuthentication;
489 $PMail->Host = self::$Server;
490 $PMail->Port = self::$Port;
491 $PMail->Username = self::$UserName;
492 $PMail->Password = self::$Password;
497 $SettingsOkay = $PMail->SmtpConnect();
502 # translate PHPMailer error message to possibly bad settings
503 switch ($Except->getMessage())
505 case 'SMTP Error: Could not authenticate.':
506 self::$DeliverySettingErrorList = array(
513 case 'SMTP Error: Could not connect to SMTP host.':
514 self::$DeliverySettingErrorList = array(
520 case 'Language string failed to load: tls':
521 self::$DeliverySettingErrorList = array(
"TLS");
525 self::$DeliverySettingErrorList = array(
"UNKNOWN");
529 # make sure failure is reported
530 $SettingsOkay = FALSE;
535 # report result to caller
536 return $SettingsOkay;
545 return self::$DeliverySettingErrorList;
549 # ---- PRIVATE INTERFACE -------------------------------------------------
552 private $ReplyTo =
"";
553 private $To = array();
554 private $CC = array();
555 private $BCC = array();
557 private $AlternateBody =
"";
558 private $Subject =
"";
559 private $Headers = array();
561 private static $LineEnding =
"\r\n";
563 private static $DeliveryMethod = self::METHOD_PHPMAIL;
564 private static $DeliverySettingErrorList = array();
565 private static $Server;
566 private static $Port = 25;
567 private static $UserName =
"";
568 private static $Password =
"";
569 private static $UseAuthentication = FALSE;
571 private function SendViaPhpMail()
573 # make lines using the line ending variable a bit shorter
574 $LE = self::$LineEnding;
576 # build basic headers list
577 $Headers =
"From: ".self::CleanHeaderValue($this->
From).$LE;
578 $Headers .= $this->BuildAddresseeLine(
"Cc", $this->
CC);
579 $Headers .= $this->BuildAddresseeLine(
"Bcc", $this->
BCC);
580 $Headers .=
"Reply-To: ".self::CleanHeaderValue(
583 # add additional headers
584 foreach ($this->Headers as $ExtraHeader)
586 $Headers .= $ExtraHeader.$LE;
589 # build recipient list
592 foreach ($this->
To as $Recipient)
594 $To .= $Separator.$Recipient;
598 # normalize message body line endings
599 $Body = $this->NormalizeLineEndings($this->
Body, $LE);
602 $Result = mail($To, $this->
Subject, $Body, $Headers);
604 # report to caller whether attempt to send succeeded
613 private function SendViaPhpMailer()
615 # create and initialize PHPMailer
617 $PMail->LE = self::$LineEnding;
618 $PMail->Subject = $this->Subject;
619 $PMail->Body = $this->Body;
620 $PMail->IsHTML(FALSE);
622 # default values for the sender's name and address
624 $Address = $this->From;
626 # if the address contains a name and address, they need to extracted
627 # because PHPMailer requires that they are set as two different
629 if (preg_match(
"/ </", $this->From))
631 $Pieces = explode(
" ", $this->From);
632 $Address = array_pop($Pieces);
633 $Address = preg_replace(
"/[<>]+/",
"", $Address);
634 $Name = trim(implode($Pieces,
" "));
638 $PMail->SetFrom($Address, $Name);
641 foreach ($this->
To as $Recipient)
643 $PMail->AddAddress($Recipient);
646 # add any extra header lines
647 foreach ($this->Headers as $ExtraHeader)
649 $PMail->AddCustomHeader($ExtraHeader);
652 # add the charset if it's set
655 $PMail->CharSet = strtolower($this->
CharSet);
658 # add the alternate plain-text body if it's set
659 if ($this->HasAlternateBody())
661 $PMail->AltBody = $this->AlternateBody;
664 # set up SMTP if necessary
665 if (self::$DeliveryMethod == self::METHOD_SMTP)
668 $PMail->SMTPAuth = self::$UseAuthentication;
669 $PMail->Host = self::$Server;
670 $PMail->Port = self::$Port;
671 $PMail->Username = self::$UserName;
672 $PMail->Password = self::$Password;
676 $Result = $PMail->Send();
678 # report to caller whether attempt to send succeeded
686 private function SendViaSmtp()
688 # send via PHPMailer because it's capable of handling SMTP
689 return $this->SendViaPhpMailer();
698 private function BuildAddresseeLine($Label, $Recipients)
701 if (count($Recipients))
703 $Line .= $Label.
": ";
705 foreach ($Recipients as $Recipient)
707 $Line .= $Separator.self::CleanHeaderValue($Recipient);
710 $Line .= self::$LineEnding;
719 private function HasAlternateBody()
729 private static function CleanHeaderValue($Value)
731 # (regular expression taken from sanitizeHeaders() function in
733 return preg_replace(
'=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
743 private static function NormalizeLineEndings($Value, $LineEnding)
745 return preg_replace(
'/\r\n|\r|\n/', $LineEnding, $Value);
764 $HtmlLength = strlen($Html);
766 # tags that should have their inner HTML left alone
767 $IgnoredTags = array(
'script',
'style',
'textarea',
'title');
769 # values for determining context
771 $InClosingTag = FALSE;
772 $InIgnoredTag = FALSE;
773 $InAttribute = FALSE;
775 $IgnoredTagName = NULL;
776 $AttributeDelimiter = NULL;
778 # loop through each character of the string
779 for ($i = 0; $i < $HtmlLength; $i++)
784 if ($Char ==
"<" && !$InTag)
787 $InAttribute = FALSE;
788 $AttributeDelimiter = NULL;
790 # do some lookaheads to get the tag name and to see if the tag
792 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
794 # moving into an ignored tag
795 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
797 $InIgnoredTag = TRUE;
798 $IgnoredTagName = $TagName;
805 if ($Char ==
">" && $InTag && !$InAttribute)
807 # moving out of an ignored tag
808 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
810 $InIgnoredTag = FALSE;
811 $IgnoredTagName = NULL;
815 $InClosingTag = FALSE;
816 $InAttribute = FALSE;
818 $AttributeDelimiter = NULL;
823 # attribute delimiter characters
824 if ($Char ==
"'" || $Char ==
'"')
826 # beginning of an attribute
830 $AttributeDelimiter = $Char;
834 # end of the attribute
835 if ($InAttribute && $Char == $AttributeDelimiter)
837 $InAttribute = FALSE;
838 $AttributeDelimiter = NULL;
843 # whitespace inside of a tag but outside of an attribute can be
844 # safely converted to a newline
845 if ($InTag && !$InAttribute && preg_match(
'/\s/', $Char))
847 $Html{$i} = $LineEnding;
851 # whitespace outside of a tag can be safely converted to a newline
852 # when not in one of the ignored tags, but only do so if horizontal
853 # space is at a premium because it can make the resulting HTML
855 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match(
'/\s/', $Char))
857 $Html{$i} = $LineEnding;
875 $HtmlLength = strlen($Html);
877 # default return values
878 $InClosingTag = FALSE;
881 # if at the end of the string and lookaheads aren't possible
882 if ($TagBegin + 1 >= $HtmlLength)
884 return array($InClosingTag, $TagName);
887 # do a lookahead for whether it's a closing tag
888 if ($Html{$TagBegin+1} ==
"/")
890 $InClosingTag = TRUE;
893 # determine whether to offset by one or two to get the tag name
894 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
896 # do a lookahead for the tag name
897 for ($i = $TagStart; $i < $HtmlLength; $i++)
901 # stop getting the tag name if whitespace is found and something is
902 # available for the tag name
903 if (strlen($TagName) && preg_match(
'/[\r\n\s]/', $Char))
908 # stop getting the tag name if the character is >
918 if (substr($TagName, 0, 3) ==
"!--")
920 return array($InClosingTag,
"!--");
923 # remove characters that aren't part of a valid tag name
924 $TagName = preg_replace(
'/[^a-zA-Z0-9]/',
'', $TagName);
926 return array($InClosingTag, $TagName);