CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

Email.php
Go to the documentation of this file.
1 <?PHP
2
3 #
4 # FILE: Email.php
5 #
6 # Part of the ScoutLib application support library
7 # Copyright 2012 Edward Almasy and Internet Scout
8 # http://scout.wisc.edu
9 #
10
15 class Email {
16
17 # ---- PUBLIC INTERFACE --------------------------------------------------
18
22 function __construct()
23 {
24 }
25
26 /*@(*/
28
34 function Send()
35 {
36 switch (self::$DeliveryMethod)
37 {
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())
42 {
43 $Result = $this->SendViaPhpMailer();
44 }
45
46 # otherwise, just use the built-in mail() function
47 else
48 {
49 $Result = $this->SendViaPhpMail();
50 }
51 break;
52
53 case self::METHOD_SMTP:
54 $Result = $this->SendViaSmtp();
55 break;
56 }
57 return $Result;
58 }
59
60 /*@(*/
62
68 function Body($NewValue = NULL)
69 {
70 if ($NewValue !== NULL) { $this->Body = $NewValue; }
71 return $this->Body;
72 }
73
79 public function AlternateBody($NewValue = NULL)
80 {
81 # set the plain-text alternative if a parameter is given
82 if (func_num_args() > 0)
83 {
84 $this->AlternateBody = $NewValue;
85 }
86
87 return $this->AlternateBody;
88 }
89
95 function Subject($NewValue = NULL)
96 {
97 if ($NewValue !== NULL) { $this->Subject = $NewValue; }
98 return $this->Subject;
99 }
100
109 function From($NewAddress = NULL, $NewName = NULL)
110 {
111 if ($NewAddress !== NULL)
112 {
113 $NewAddress = trim($NewAddress);
114 if ($NewName !== NULL)
115 {
116 $NewName = trim($NewName);
117 $this->From = $NewName." <".$NewAddress.">";
118 }
119 else
120 {
121 $this->From = $NewAddress;
122 }
123 }
124 return $this->From;
125 }
126
135 function ReplyTo($NewAddress = NULL, $NewName = NULL)
136 {
137 if ($NewAddress !== NULL)
138 {
139 $NewAddress = trim($NewAddress);
140 if ($NewName !== NULL)
141 {
142 $NewName = trim($NewName);
143 $this->ReplyTo = $NewName." <".$NewAddress.">";
144 }
145 else
146 {
147 $this->ReplyTo = $NewAddress;
148 }
149 }
150 return $this->ReplyTo;
151 }
152
160 function To($NewValue = NULL)
161 {
162 if ($NewValue !== NULL)
163 {
164 if (!is_array($NewValue))
165 {
166 $this->To = array($NewValue);
167 }
168 else
169 {
170 $this->To = $NewValue;
171 }
172 }
173 return $this->To;
174 }
175
183 function CC($NewValue = NULL)
184 {
185 if ($NewValue !== NULL)
186 {
187 if (!is_array($NewValue))
188 {
189 $this->CC = array($NewValue);
190 }
191 else
192 {
193 $this->CC = $NewValue;
194 }
195 }
196 return $this->CC;
197 }
198
206 function BCC($NewValue = NULL)
207 {
208 if ($NewValue !== NULL)
209 {
210 if (!is_array($NewValue))
211 {
212 $this->BCC = array($NewValue);
213 }
214 else
215 {
216 $this->BCC = $NewValue;
217 }
218 }
219 return $this->BCC;
220 }
221
226 function AddHeaders($NewHeaders)
227 {
228 # add new headers to list
229 $this->Headers = array_merge($this->Headers, $NewHeaders);
230 }
231
238 public function CharSet($NewValue = NULL)
239 {
240 # set the plain-text alternative if a parameter is given
241 if (func_num_args() > 0)
242 {
243 $this->CharSet = $NewValue;
244 }
245
246 return $this->CharSet;
247 }
248
254 public static function LineEnding($NewValue = NULL)
255 {
256 if (!is_null($NewValue))
257 {
258 self::$LineEnding = $NewValue;
259 }
260
261 return self::$LineEnding;
262 }
263
276 public static function WrapHtmlAsNecessary($Html, $MaxLineLength=998, $LineEnding="\r\n")
277 {
278 # the regular expression used to find long lines
279 $LongLineRegExp = '/[^\r\n]{'.($MaxLineLength+1).',}/';
280
281 # find all lines that are too long
282 preg_match_all($LongLineRegExp, $Html, $Matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
283
284 # no changes are necessary
285 if (!count($Matches))
286 {
287 return $Html;
288 }
289
290 # go backwards so that the HTML can be edited in place without messing
291 # with the offsets
292 for ($i = count($Matches[0]) - 1; $i >= 0; $i--)
293 {
294 # extract the line text and its offset within the string
295 list($Line, $Offset) = $Matches[0][$i];
296
297 # first try to get the line under the limit without being too
298 # aggressive
299 $BetterLine = self::ConvertHtmlWhiteSpace($Line, FALSE, $LineEnding);
300 $WasAggressive = "No";
301
302 # if the line is still too long, be more aggressive with replacing
303 # horizontal whitespace
304 if (preg_match($LongLineRegExp, $BetterLine))
305 {
306 $BetterLine = self::ConvertHtmlWhiteSpace($Line, TRUE, $LineEnding);
307 $WasAggressive = "Yes";
308 }
309
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;
315
316 # replace the line within the HTML
317 $Html = substr_replace($Html, $BetterLine, $Offset, strlen($Line));
318 }
319
320 return $Html;
321 }
322
330 public static function TestLineEndings($Value, $LineEnding)
331 {
332 # the number of \r in the string
333 $NumCR = substr_count($Value, "\r");
334
335 # LF
336 if ($LineEnding == "\n")
337 {
338 return $NumCR === 0;
339 }
340
341 # the number of \n in the string
342 $NumLF = substr_count($Value, "\n");
343
344 # CR
345 if ($LineEnding == "\r")
346 {
347 return $NumLF === 0;
348 }
349
350 # the number of \r\n in the string
351 $NumCRLF = substr_count($Value, "\r\n");
352
353 # CRLF. also check CRLF to make sure CR and LF appear together and in
354 # the correct order
355 return $NumCR === $NumLF && $NumLF === $NumCRLF;
356 }
357 /*@(*/
359
366 static function DeliveryMethod($NewValue = NULL)
367 {
368 if ($NewValue !== NULL)
369 {
370 self::$DeliveryMethod = $NewValue;
371 }
372 return self::$DeliveryMethod;
373 }
374 const METHOD_PHPMAIL = 1;
375 const METHOD_SMTP = 2;
376
382 static function Server($NewValue = NULL)
383 {
384 if ($NewValue !== NULL) { self::$Server = $NewValue; }
385 return self::$Server;
386 }
387
393 static function Port($NewValue = NULL)
394 {
395 if ($NewValue !== NULL) { self::$Port = $NewValue; }
396 return self::$Port;
397 }
398
404 static function UserName($NewValue = NULL)
405 {
406 if ($NewValue !== NULL) { self::$UserName = $NewValue; }
407 return self::$UserName;
408 }
409
415 static function Password($NewValue = NULL)
416 {
417 if ($NewValue !== NULL) { self::$Password = $NewValue; }
418 return self::$Password;
419 }
420
426 static function UseAuthentication($NewValue = NULL)
427 {
428 if ($NewValue !== NULL) { self::$UseAuthentication = $NewValue; }
429 return self::$UseAuthentication;
430 }
431
439 static function DeliverySettings($NewSettings = NULL)
440 {
441 if ($NewSettings !== NULL)
442 {
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"];
450 }
451 else
452 {
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;
459 }
460 return serialize($Settings);
461 }
462
471 static function DeliverySettingsOkay()
472 {
473 # start out with error list clear
474 self::$DeliverySettingErrorList = array();
475
476 # test based on delivery method
477 switch (self::$DeliveryMethod)
478 {
479 case self::METHOD_PHPMAIL:
480 # always report success
481 $SettingsOkay = TRUE;
482 break;
483
484 case self::METHOD_SMTP:
485 # set up PHPMailer for test
486 $PMail = new PHPMailer(TRUE);
487 $PMail->IsSMTP();
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;
493
494 # test settings
495 try
496 {
497 $SettingsOkay = $PMail->SmtpConnect();
498 }
499 # if test failed
500 catch (phpmailerException $Except)
501 {
502 # translate PHPMailer error message to possibly bad settings
503 switch ($Except->getMessage())
504 {
505 case 'SMTP Error: Could not authenticate.':
506 self::$DeliverySettingErrorList = array(
507 "UseAuthentication",
508 "UserName",
509 "Password",
510 );
511 break;
512
513 case 'SMTP Error: Could not connect to SMTP host.':
514 self::$DeliverySettingErrorList = array(
515 "Server",
516 "Port",
517 );
518 break;
519
520 case 'Language string failed to load: tls':
521 self::$DeliverySettingErrorList = array("TLS");
522 break;
523
524 default:
525 self::$DeliverySettingErrorList = array("UNKNOWN");
526 break;
527 }
528
529 # make sure failure is reported
530 $SettingsOkay = FALSE;
531 }
532 break;
533 }
534
535 # report result to caller
536 return $SettingsOkay;
537 }
538
543 static function DeliverySettingErrors()
544 {
545 return self::$DeliverySettingErrorList;
546 }
547
548
549 # ---- PRIVATE INTERFACE -------------------------------------------------
550
551 private $From = "";
552 private $ReplyTo = "";
553 private $To = array();
554 private $CC = array();
555 private $BCC = array();
556 private $Body = "";
557 private $AlternateBody = "";
558 private $Subject = "";
559 private $Headers = array();
560 private $CharSet;
561 private static $LineEnding = "\r\n";
562
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;
570
571 private function SendViaPhpMail()
572 {
573 # make lines using the line ending variable a bit shorter
574 $LE = self::$LineEnding;
575
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(
581 strlen($this->ReplyTo) ? $this->ReplyTo : $this->From).$LE;
582
583 # add additional headers
584 foreach ($this->Headers as $ExtraHeader)
585 {
586 $Headers .= $ExtraHeader.$LE;
587 }
588
589 # build recipient list
590 $To = "";
591 $Separator = "";
592 foreach ($this->To as $Recipient)
593 {
594 $To .= $Separator.$Recipient;
595 $Separator = ", ";
596 }
597
598 # normalize message body line endings
599 $Body = $this->NormalizeLineEndings($this->Body, $LE);
600
601 # send message
602 $Result = mail($To, $this->Subject, $Body, $Headers);
603
604 # report to caller whether attempt to send succeeded
605 return $Result;
606 }
607
613 private function SendViaPhpMailer()
614 {
615 # create and initialize PHPMailer
616 $PMail = new PHPMailer();
617 $PMail->LE = self::$LineEnding;
618 $PMail->Subject = $this->Subject;
619 $PMail->Body = $this->Body;
620 $PMail->IsHTML(FALSE);
621
622 # default values for the sender's name and address
623 $Name = "";
624 $Address = $this->From;
625
626 # if the address contains a name and address, they need to extracted
627 # because PHPMailer requires that they are set as two different
628 # parameters
629 if (preg_match("/ </", $this->From))
630 {
631 $Pieces = explode(" ", $this->From);
632 $Address = array_pop($Pieces);
633 $Address = preg_replace("/[<>]+/", "", $Address);
634 $Name = trim(implode($Pieces, " "));
635 }
636
637 # add the sender
638 $PMail->SetFrom($Address, $Name);
639
640 # add each recipient
641 foreach ($this->To as $Recipient)
642 {
643 $PMail->AddAddress($Recipient);
644 }
645
646 # add any extra header lines
647 foreach ($this->Headers as $ExtraHeader)
648 {
649 $PMail->AddCustomHeader($ExtraHeader);
650 }
651
652 # add the charset if it's set
653 if (isset($this->CharSet))
654 {
655 $PMail->CharSet = strtolower($this->CharSet);
656 }
657
658 # add the alternate plain-text body if it's set
659 if ($this->HasAlternateBody())
660 {
661 $PMail->AltBody = $this->AlternateBody;
662 }
663
664 # set up SMTP if necessary
665 if (self::$DeliveryMethod == self::METHOD_SMTP)
666 {
667 $PMail->IsSMTP();
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;
673 }
674
675 # send message
676 $Result = $PMail->Send();
677
678 # report to caller whether attempt to send succeeded
679 return $Result;
680 }
681
686 private function SendViaSmtp()
687 {
688 # send via PHPMailer because it's capable of handling SMTP
689 return $this->SendViaPhpMailer();
690 }
691
698 private function BuildAddresseeLine($Label, $Recipients)
699 {
700 $Line = "";
701 if (count($Recipients))
702 {
703 $Line .= $Label.": ";
704 $Separator = "";
705 foreach ($Recipients as $Recipient)
706 {
707 $Line .= $Separator.self::CleanHeaderValue($Recipient);
708 $Separator = ", ";
709 }
710 $Line .= self::$LineEnding;
711 }
712 return $Line;
713 }
714
719 private function HasAlternateBody()
720 {
721 return isset($this->AlternateBody) && strlen(trim($this->AlternateBody)) > 0;
722 }
723
729 private static function CleanHeaderValue($Value)
730 {
731 # (regular expression taken from sanitizeHeaders() function in
732 # Mail PEAR package)
733 return preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
734 "", $Value);
735 }
736
743 private static function NormalizeLineEndings($Value, $LineEnding)
744 {
745 return preg_replace('/\r\n|\r|\n/', $LineEnding, $Value);
746 }
747
762 protected static function ConvertHtmlWhiteSpace($Html, $Aggressive=FALSE, $LineEnding="\r\n")
763 {
764 $HtmlLength = strlen($Html);
765
766 # tags that should have their inner HTML left alone
767 $IgnoredTags = array('script', 'style', 'textarea', 'title');
768
769 # values for determining context
770 $InTag = FALSE;
771 $InClosingTag = FALSE;
772 $InIgnoredTag = FALSE;
773 $InAttribute = FALSE;
774 $TagName = NULL;
775 $IgnoredTagName = NULL;
776 $AttributeDelimiter = NULL;
777
778 # loop through each character of the string
779 for ($i = 0; $i < $HtmlLength; $i++)
780 {
781 $Char = $Html{$i};
782
783 # beginning of a tag
784 if ($Char == "<" && !$InTag)
785 {
786 $InTag = TRUE;
787 $InAttribute = FALSE;
788 $AttributeDelimiter = NULL;
789
790 # do some lookaheads to get the tag name and to see if the tag
791 # is a closing tag
792 list($InClosingTag, $TagName) = self::GetTagInfo($Html, $i);
793
794 # moving into an ignored tag
795 if (!$InClosingTag && in_array($TagName, $IgnoredTags))
796 {
797 $InIgnoredTag = TRUE;
798 $IgnoredTagName = $TagName;
799 }
800
801 continue;
802 }
803
804 # end of a tag
805 if ($Char == ">" && $InTag && !$InAttribute)
806 {
807 # moving out of an ignored tag
808 if ($InClosingTag && $InIgnoredTag && $TagName == $IgnoredTagName)
809 {
810 $InIgnoredTag = FALSE;
811 $IgnoredTagName = NULL;
812 }
813
814 $InTag = FALSE;
815 $InClosingTag = FALSE;
816 $InAttribute = FALSE;
817 $TagName = NULL;
818 $AttributeDelimiter = NULL;
819
820 continue;
821 }
822
823 # attribute delimiter characters
824 if ($Char == "'" || $Char == '"')
825 {
826 # beginning of an attribute
827 if (!$InAttribute)
828 {
829 $InAttribute = TRUE;
830 $AttributeDelimiter = $Char;
831 continue;
832 }
833
834 # end of the attribute
835 if ($InAttribute && $Char == $AttributeDelimiter)
836 {
837 $InAttribute = FALSE;
838 $AttributeDelimiter = NULL;
839 continue;
840 }
841 }
842
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))
846 {
847 $Html{$i} = $LineEnding;
848 continue;
849 }
850
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
854 # difficult to read
855 if ($Aggressive && !$InTag && !$InIgnoredTag && preg_match('/\s/', $Char))
856 {
857 $Html{$i} = $LineEnding;
858 continue;
859 }
860 }
861
862 return $Html;
863 }
864
873 protected static function GetTagInfo($Html, $TagBegin)
874 {
875 $HtmlLength = strlen($Html);
876
877 # default return values
878 $InClosingTag = FALSE;
879 $TagName = NULL;
880
881 # if at the end of the string and lookaheads aren't possible
882 if ($TagBegin + 1 >= $HtmlLength)
883 {
884 return array($InClosingTag, $TagName);
885 }
886
887 # do a lookahead for whether it's a closing tag
888 if ($Html{$TagBegin+1} == "/")
889 {
890 $InClosingTag = TRUE;
891 }
892
893 # determine whether to offset by one or two to get the tag name
894 $TagStart = $InClosingTag ? $TagBegin + 2 : $TagBegin + 1;
895
896 # do a lookahead for the tag name
897 for ($i = $TagStart; $i < $HtmlLength; $i++)
898 {
899 $Char = $Html{$i};
900
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))
904 {
905 break;
906 }
907
908 # stop getting the tag name if the character is >
909 if ($Char == ">")
910 {
911 break;
912 }
913
914 $TagName .= $Char;
915 }
916
917 # comment "tag"
918 if (substr($TagName, 0, 3) == "!--")
919 {
920 return array($InClosingTag, "!--");
921 }
922
923 # remove characters that aren't part of a valid tag name
924 $TagName = preg_replace('/[^a-zA-Z0-9]/', '', $TagName);
925
926 return array($InClosingTag, $TagName);
927 }
928
929 }