CWIS Developer Documentation
Email.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: Email.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2012-2013 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 class Email {
15 
16  # ---- PUBLIC INTERFACE --------------------------------------------------
17  /*@(*/
19 
25  function Send()
26  {
27  switch (self::$DeliveryMethod)
28  {
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())
33  {
34  $Result = $this->SendViaPhpMailerLib();
35  }
36 
37  # otherwise, just use the built-in mail() function
38  else
39  {
40  $Result = $this->SendViaPhpMailFunc();
41  }
42  break;
43 
44  case self::METHOD_SMTP:
45  $Result = $this->SendViaSmtp();
46  break;
47  }
48  return $Result;
49  }
50 
51  /*@(*/
53 
59  function Body($NewValue = NULL)
60  {
61  if ($NewValue !== NULL) { $this->Body = $NewValue; }
62  return $this->Body;
63  }
64 
70  public function AlternateBody($NewValue = NULL)
71  {
72  # set the plain-text alternative if a parameter is given
73  if (func_num_args() > 0)
74  {
75  $this->AlternateBody = $NewValue;
76  }
77 
78  return $this->AlternateBody;
79  }
80 
86  function Subject($NewValue = NULL)
87  {
88  if ($NewValue !== NULL) { $this->Subject = $NewValue; }
89  return $this->Subject;
90  }
91 
100  function From($NewAddress = NULL, $NewName = NULL)
101  {
102  if ($NewAddress !== NULL)
103  {
104  $NewAddress = trim($NewAddress);
105  if ($NewName !== NULL)
106  {
107  $NewName = trim($NewName);
108  $this->From = $NewName." <".$NewAddress.">";
109  }
110  else
111  {
112  $this->From = $NewAddress;
113  }
114  }
115  return $this->From;
116  }
117 
124  static function DefaultFrom($NewValue = NULL)
125  {
126  if ($NewValue !== NULL) { self::$DefaultFrom = $NewValue; }
127  return self::$DefaultFrom;
128  }
129 
138  function ReplyTo($NewAddress = NULL, $NewName = NULL)
139  {
140  if ($NewAddress !== NULL)
141  {
142  $NewAddress = trim($NewAddress);
143  if ($NewName !== NULL)
144  {
145  $NewName = trim($NewName);
146  $this->ReplyTo = $NewName." <".$NewAddress.">";
147  }
148  else
149  {
150  $this->ReplyTo = $NewAddress;
151  }
152  }
153  return $this->ReplyTo;
154  }
155 
163  function To($NewValue = NULL)
164  {
165  if ($NewValue !== NULL)
166  {
167  if (!is_array($NewValue))
168  {
169  $this->To = array($NewValue);
170  }
171  else
172  {
173  $this->To = $NewValue;
174  }
175  }
176  return $this->To;
177  }
178 
186  function CC($NewValue = NULL)
187  {
188  if ($NewValue !== NULL)
189  {
190  if (!is_array($NewValue))
191  {
192  $this->CC = array($NewValue);
193  }
194  else
195  {
196  $this->CC = $NewValue;
197  }
198  }
199  return $this->CC;
200  }
201 
209  function BCC($NewValue = NULL)
210  {
211  if ($NewValue !== NULL)
212  {
213  if (!is_array($NewValue))
214  {
215  $this->BCC = array($NewValue);
216  }
217  else
218  {
219  $this->BCC = $NewValue;
220  }
221  }
222  return $this->BCC;
223  }
224 
229  function AddHeaders($NewHeaders)
230  {
231  # add new headers to list
232  $this->Headers = array_merge($this->Headers, $NewHeaders);
233  }
234 
241  public function CharSet($NewValue = NULL)
242  {
243  # set the plain-text alternative if a parameter is given
244  if (func_num_args() > 0)
245  {
246  $this->CharSet = $NewValue;
247  }
248 
249  return $this->CharSet;
250  }
251 
257  public static function LineEnding($NewValue = NULL)
258  {
259  if (!is_null($NewValue))
260  {
261  self::$LineEnding = $NewValue;
262  }
263 
264  return self::$LineEnding;
265  }
266 
279  public static function WrapHtmlAsNecessary($Html, $MaxLineLength=998, $LineEnding="\r\n")
280  {
281  # the regular expression used to find long lines
282  $LongLineRegExp = '/[^\r\n]{'.($MaxLineLength+1).',}/';
283 
284  # find all lines that are too long
285  preg_match_all($LongLineRegExp, $Html, $Matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
286 
287  # no changes are necessary
288  if (!count($Matches))
289  {
290  return $Html;
291  }
292 
293  # go backwards so that the HTML can be edited in place without messing
294  # with the offsets
295  for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
296  {
297  # extract the line text and its offset within the string
298  list($Line, $Offset) = $Matches[0][$i];
299 
300  # first try to get the line under the limit without being too
301  # aggressive
302  $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
303  $WasAggressive = "No";
304 
305  # if the line is still too long, be more aggressive with replacing
306  # horizontal whitespace
307  if (preg_match($LongLineRegExp, $BetterLine))
308  {
309  $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
310  $WasAggressive = "Yes";
311  }
312 
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;
318 
319  # replace the line within the HTML
320  $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
321  }
322 
323  return $Html;
324  }
325 
333  public static function TestLineEndings($Value, $LineEnding)
334  {
335  # the number of \r in the string
336  $NumCR = substr_count($Value, "\r");
337 
338  # LF
339  if ($LineEnding == "\n")
340  {
341  return $NumCR === 0;
342  }
343 
344  # the number of \n in the string
345  $NumLF = substr_count($Value, "\n");
346 
347  # CR
348  if ($LineEnding == "\r")
349  {
350  return $NumLF === 0;
351  }
352 
353  # the number of \r\n in the string
354  $NumCRLF = substr_count($Value, "\r\n");
355 
356  # CRLF. also check CRLF to make sure CR and LF appear together and in
357  # the correct order
358  return $NumCR === $NumLF && $NumLF === $NumCRLF;
359  }
360 
366  public static function ConvertHtmlToPlainText($Html)
367  {
368  # remove newlines
369  $Text = str_replace(array("\r", "\n"), "", $Html);
370 
371  # convert HTML breaks to newlines
372  $Text = preg_replace('/<br\s*\/?>/', "\n", $Text);
373 
374  # strip remaining tags
375  $Text = strip_tags($Text);
376 
377  # convert HTML entities to their plain-text equivalents
378  $Text = html_entity_decode($Text);
379 
380  # single quotes aren't always handled
381  $Text = str_replace('&#39;', "'", $Text);
382 
383  # remove HTML entities that have no equivalents
384  $Text = preg_replace('/&(#[0-9]{1,6}|[a-zA-Z0-9]{1,6});/', "", $Text);
385 
386  # return the plain text version
387  return $Text;
388  }
389  /*@(*/
391 
398  static function DeliveryMethod($NewValue = NULL)
399  {
400  if ($NewValue !== NULL)
401  {
402  self::$DeliveryMethod = $NewValue;
403  }
404  return self::$DeliveryMethod;
405  }
407  const METHOD_PHPMAIL = 1;
409  const METHOD_SMTP = 2;
410 
416  static function Server($NewValue = NULL)
417  {
418  if ($NewValue !== NULL) { self::$Server = $NewValue; }
419  return self::$Server;
420  }
421 
427  static function Port($NewValue = NULL)
428  {
429  if ($NewValue !== NULL) { self::$Port = $NewValue; }
430  return self::$Port;
431  }
432 
438  static function UserName($NewValue = NULL)
439  {
440  if ($NewValue !== NULL) { self::$UserName = $NewValue; }
441  return self::$UserName;
442  }
443 
449  static function Password($NewValue = NULL)
450  {
451  if ($NewValue !== NULL) { self::$Password = $NewValue; }
452  return self::$Password;
453  }
454 
460  static function UseAuthentication($NewValue = NULL)
461  {
462  if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
463  return self::$UseAuthentication;
464  }
465 
473  static function DeliverySettings($NewSettings = NULL)
474  {
475  if ($NewSettings !== NULL)
476  {
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"];
484  }
485  else
486  {
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;
493  }
494  return serialize($Settings);
495  }
496 
505  static function DeliverySettingsOkay()
506  {
507  # start out with error list clear
508  self::$DeliverySettingErrorList = array();
509 
510  # test based on delivery method
511  switch (self::$DeliveryMethod)
512  {
513  case self::METHOD_PHPMAIL:
514  # always report success
515  $SettingsOkay = TRUE;
516  break;
517 
518  case self::METHOD_SMTP:
519  # set up PHPMailer for test
520  $PMail = new PHPMailer(TRUE);
521  $PMail->IsSMTP();
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;
527 
528  # test settings
529  try
530  {
531  $SettingsOkay = $PMail->SmtpConnect();
532  }
533  # if test failed
534  catch (phpmailerException $Except)
535  {
536  # translate PHPMailer error message to possibly bad settings
537  switch ($Except->getMessage())
538  {
539  case 'SMTP Error: Could not authenticate.':
540  self::$DeliverySettingErrorList = array(
541  "UseAuthentication",
542  "UserName",
543  "Password",
544  );
545  break;
546 
547  case 'SMTP Error: Could not connect to SMTP host.':
548  self::$DeliverySettingErrorList = array(
549  "Server",
550  "Port",
551  );
552  break;
553 
554  case 'Language string failed to load: tls':
555  self::$DeliverySettingErrorList = array("TLS");
556  break;
557 
558  default:
559  self::$DeliverySettingErrorList = array("UNKNOWN");
560  break;
561  }
562 
563  # make sure failure is reported
564  $SettingsOkay = FALSE;
565  }
566  break;
567  }
568 
569  # report result to caller
570  return $SettingsOkay;
571  }
572 
577  static function DeliverySettingErrors()
578  {
579  return self::$DeliverySettingErrorList;
580  }
581 
582 
583  # ---- PRIVATE INTERFACE -------------------------------------------------
584 
585  private $From = "";
586  private $ReplyTo = "";
587  private $To = array();
588  private $CC = array();
589  private $BCC = array();
590  private $Body = "";
591  private $AlternateBody = "";
592  private $Subject = "";
593  private $Headers = array();
594  private $CharSet;
595  private static $LineEnding = "\r\n";
596  private static $DefaultFrom = "";
597 
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;
605 
610  private function SendViaPhpMailFunc()
611  {
612  # make lines using the line ending variable a bit shorter
613  $LE = self::$LineEnding;
614 
615  # build basic headers list
616  $From = strlen($this->From) ? $this->From : self::$DefaultFrom;
617  $Headers = "From: ".self::CleanHeaderValue($From).$LE;
618  $Headers .= $this->BuildAddresseeLine("Cc", $this->CC);
619  $Headers .= $this->BuildAddresseeLine("Bcc", $this->BCC);
620  $Headers .= "Reply-To: ".self::CleanHeaderValue(
621  strlen($this->ReplyTo) ? $this->ReplyTo : $this->From).$LE;
622 
623  # add additional headers
624  foreach ($this->Headers as $ExtraHeader)
625  {
626  $Headers .= $ExtraHeader.$LE;
627  }
628 
629  # build recipient list
630  $To = "";
631  $Separator = "";
632  foreach ($this->To as $Recipient)
633  {
634  $To .= $Separator.$Recipient;
635  $Separator = ", ";
636  }
637 
638  # normalize message body line endings
639  $Body = $this->NormalizeLineEndings($this->Body, $LE);
640 
641  # send message
642  $Result = mail($To, $this->Subject, $Body, $Headers);
643 
644  # report to caller whether attempt to send succeeded
645  return $Result;
646  }
647 
653  private function SendViaPhpMailerLib()
654  {
655  # create and initialize PHPMailer
656  $PMail = new PHPMailer();
657  $PMail->LE = self::$LineEnding;
658  $PMail->Subject = $this->Subject;
659  $PMail->Body = $this->Body;
660  $PMail->IsHTML(FALSE);
661 
662  # default values for the sender's name and address
663  $Name = "";
664  $Address = $this->From;
665 
666  # if the address contains a name and address, they need to extracted
667  # because PHPMailer requires that they are set as two different
668  # parameters
669  if (preg_match("/ </", $this->From))
670  {
671  $Pieces = explode(" ", $this->From);
672  $Address = array_pop($Pieces);
673  $Address = preg_replace("/[<>]+/", "", $Address);
674  $Name = trim(implode($Pieces, " "));
675  }
676 
677  # add the sender
678  $PMail->SetFrom($Address, $Name);
679 
680  # add each recipient
681  foreach ($this->To as $Recipient)
682  {
683  $PMail->AddAddress($Recipient);
684  }
685 
686  # add any extra header lines
687  foreach ($this->Headers as $ExtraHeader)
688  {
689  $PMail->AddCustomHeader($ExtraHeader);
690  }
691 
692  # add the charset if it's set
693  if (isset($this->CharSet))
694  {
695  $PMail->CharSet = strtolower($this->CharSet);
696  }
697 
698  # add the alternate plain-text body if it's set
699  if ($this->HasAlternateBody())
700  {
701  $PMail->AltBody = $this->AlternateBody;
702  }
703 
704  # set up SMTP if necessary
705  if (self::$DeliveryMethod == self::METHOD_SMTP)
706  {
707  $PMail->IsSMTP();
708  $PMail->SMTPAuth = self::$UseAuthentication;
709  $PMail->Host = self::$Server;
710  $PMail->Port = self::$Port;
711  $PMail->Username = self::$UserName;
712  $PMail->Password = self::$Password;
713  }
714 
715  # send message
716  $Result = $PMail->Send();
717 
718  # report to caller whether attempt to send succeeded
719  return $Result;
720  }
721 
726  private function SendViaSmtp()
727  {
728  # send via PHPMailer because it's capable of handling SMTP
729  return $this->SendViaPhpMailerLib();
730  }
731 
738  private function BuildAddresseeLine($Label, $Recipients)
739  {
740  $Line = "";
741  if (count($Recipients))
742  {
743  $Line .= $Label.": ";
744  $Separator = "";
745  foreach ($Recipients as $Recipient)
746  {
747  $Line .= $Separator.self::CleanHeaderValue($Recipient);
748  $Separator = ", ";
749  }
750  $Line .= self::$LineEnding;
751  }
752  return $Line;
753  }
754 
759  private function HasAlternateBody()
760  {
761  return isset($this->AlternateBody) && strlen(trim($this->AlternateBody)) > 0;
762  }
763 
769  private static function CleanHeaderValue($Value)
770  {
771  # (regular expression taken from sanitizeHeaders() function in
772  # Mail PEAR package)
773  return preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
774  "", $Value);
775  }
776 
783  private static function NormalizeLineEndings($Value, $LineEnding)
784  {
785  return preg_replace('/\r\n|\r|\n/', $LineEnding, $Value);
786  }
787 
802  protected static function ConvertHtmlWhiteSpace($Html, $Aggressive=FALSE, $LineEnding="\r\n")
803  {
804  $HtmlLength = strlen($Html);
805 
806  # tags that should have their inner HTML left alone
807  $IgnoredTags = array('script', 'style', 'textarea', 'title');
808 
809  # values for determining context
810  $InTag = FALSE;
811  $InClosingTag = FALSE;
812  $InIgnoredTag = FALSE;
813  $InAttribute = FALSE;
814  $TagName = NULL;
815  $IgnoredTagName = NULL;
816  $AttributeDelimiter = NULL;
817 
818  # loop through each character of the string
819  for ($i = 0; $i < $HtmlLength; $i++)
820  {
821  $Char = $Html{$i};
822 
823  # beginning of a tag
824  if ($Char == "<" && !$InTag)
825  {
826  $InTag = TRUE;
827  $InAttribute = FALSE;
828  $AttributeDelimiter = NULL;
829 
830  # do some lookaheads to get the tag name and to see if the tag
831  # is a closing tag
832  list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
833 
834  # moving into an ignored tag
835  if (!$InClosingTag && in_array($TagName, $IgnoredTags))
836  {
837  $InIgnoredTag = TRUE;
838  $IgnoredTagName = $TagName;
839  }
840 
841  continue;
842  }
843 
844  # end of a tag
845  if ($Char == ">" && $InTag && !$InAttribute)
846  {
847  # moving out of an ignored tag
848  if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
849  {
850  $InIgnoredTag = FALSE;
851  $IgnoredTagName = NULL;
852  }
853 
854  $InTag = FALSE;
855  $InClosingTag = FALSE;
856  $InAttribute = FALSE;
857  $TagName = NULL;
858  $AttributeDelimiter = NULL;
859 
860  continue;
861  }
862 
863  # attribute delimiter characters
864  if ($Char == "'" || $Char == '"')
865  {
866  # beginning of an attribute
867  if (!$InAttribute)
868  {
869  $InAttribute = TRUE;
870  $AttributeDelimiter = $Char;
871  continue;
872  }
873 
874  # end of the attribute
875  if ($InAttribute && $Char == $AttributeDelimiter)
876  {
877  $InAttribute = FALSE;
878  $AttributeDelimiter = NULL;
879  continue;
880  }
881  }
882 
883  # whitespace inside of a tag but outside of an attribute can be
884  # safely converted to a newline
885  if ($InTag && !$InAttribute && preg_match('/\s/', $Char))
886  {
887  $Html{$i} = $LineEnding;
888  continue;
889  }
890 
891  # whitespace outside of a tag can be safely converted to a newline
892  # when not in one of the ignored tags, but only do so if horizontal
893  # space is at a premium because it can make the resulting HTML
894  # difficult to read
895  if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match('/\s/', $Char))
896  {
897  $Html{$i} = $LineEnding;
898  continue;
899  }
900  }
901 
902  return $Html;
903  }
904 
913  protected static function GetTagInfo($Html, $TagBegin)
914  {
915  $HtmlLength = strlen($Html);
916 
917  # default return values
918  $InClosingTag = FALSE;
919  $TagName = NULL;
920 
921  # if at the end of the string and lookaheads aren't possible
922  if ($TagBegin + 1 >= $HtmlLength)
923  {
924  return array($InClosingTag, $TagName);
925  }
926 
927  # do a lookahead for whether it's a closing tag
928  if ($Html{$TagBegin+1} == "/")
929  {
930  $InClosingTag = TRUE;
931  }
932 
933  # determine whether to offset by one or two to get the tag name
934  $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
935 
936  # do a lookahead for the tag name
937  for ($i = $TagStart; $i < $HtmlLength; $i++)
938  {
939  $Char = $Html{$i};
940 
941  # stop getting the tag name if whitespace is found and something is
942  # available for the tag name
943  if (strlen($TagName) && preg_match('/[\r\n\s]/', $Char))
944  {
945  break;
946  }
947 
948  # stop getting the tag name if the character is >
949  if ($Char == ">")
950  {
951  break;
952  }
953 
954  $TagName .= $Char;
955  }
956 
957  # comment "tag"
958  if (substr($TagName, 0, 3) == "!--")
959  {
960  return array($InClosingTag, "!--");
961  }
962 
963  # remove characters that aren't part of a valid tag name
964  $TagName = preg_replace('/[^a-zA-Z0-9]/', '', $TagName);
965 
966  return array($InClosingTag, $TagName);
967  }
968 
969 }
From($NewAddress=NULL, $NewName=NULL)
Get/set message sender.
Definition: Email.php:100
static Server($NewValue=NULL)
Get/set server for mail delivery.
Definition: Email.php:416
static DeliveryMethod($NewValue=NULL)
Get/set mail delivery method.
Definition: Email.php:398
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.
Definition: Email.php:279
To($NewValue=NULL)
Get/set message recipient(s).
Definition: Email.php:163
static DeliverySettings($NewSettings=NULL)
Get/set serialized (opaque text) version of delivery settings.
Definition: Email.php:473
ReplyTo($NewAddress=NULL, $NewName=NULL)
Get/set message &quot;Reply-To&quot; address.
Definition: Email.php:138
static DeliverySettingsOkay()
Test delivery settings and report their validity.
Definition: Email.php:505
static LineEnding($NewValue=NULL)
Specify the character sequence that should be used to end lines.
Definition: Email.php:257
static DeliverySettingErrors()
Return array with list of delivery setting errors (if any).
Definition: Email.php:577
CharSet($NewValue=NULL)
Specify a character encoding for the message.
Definition: Email.php:241
Electronic mail message.
Definition: Email.php:14
static DefaultFrom($NewValue=NULL)
Get/set default &quot;From&quot; address.
Definition: Email.php:124
static GetTagInfo($Html, $TagBegin)
Get the tag name and whether it&#39;s a closing tag from a tag that begins at a specific offset within so...
Definition: Email.php:913
Send()
Mail the message.
Definition: Email.php:25
PHP
Definition: OAIClient.php:39
AddHeaders($NewHeaders)
Specify additional message headers to be included.
Definition: Email.php:229
const METHOD_PHPMAIL
Deliver using PHP&#39;s internal mail() mechanism.
Definition: Email.php:407
BCC($NewValue=NULL)
Get/set message BCC list.
Definition: Email.php:209
Body($NewValue=NULL)
Get/set message body.
Definition: Email.php:59
static Port($NewValue=NULL)
Get/set port number for mail delivery.
Definition: Email.php:427
static UserName($NewValue=NULL)
Get/set user name for mail delivery.
Definition: Email.php:438
AlternateBody($NewValue=NULL)
Get/set the plain-text alternative to the body.
Definition: Email.php:70
Subject($NewValue=NULL)
Get/set message subject.
Definition: Email.php:86
const METHOD_SMTP
Deliver using SMTP.
Definition: Email.php:409
static Password($NewValue=NULL)
Get/set password for mail delivery.
Definition: Email.php:449
static ConvertHtmlToPlainText($Html)
Try as best as possible to convert HTML to plain text.
Definition: Email.php:366
static UseAuthentication($NewValue=NULL)
Get/set whether to use authentication for mail delivery.
Definition: Email.php:460
static TestLineEndings($Value, $LineEnding)
Test the line endings in a value to see if they all match the given line ending.
Definition: Email.php:333
CC($NewValue=NULL)
Get/set message CC list.
Definition: Email.php:186
static ConvertHtmlWhiteSpace($Html, $Aggressive=FALSE, $LineEnding="\r\n")
Convert horizontal white space with no semantic value to vertical white space when possible...
Definition: Email.php:802