Version Description
(2016-11-01) = * Fix bug where OS detection failure was preventing email processing
Download this release
Release Info
Developer | WayneAllen |
Plugin | Postie |
Version | 1.8.14 |
Comparing to | |
See all releases |
Code changes from version 1.8.13 to 1.8.14
- docs/Changes.txt +3 -0
- docs/Postie.txt +1 -1
- lib/fCore.php +9 -19
- lib/fEmail.php +1588 -1712
- lib/fException.php +500 -542
- lib/fUnexpectedException.php +15 -16
- lib/fValidationException.php +150 -170
- lib/pImapMailServer.php +0 -23
- postie.php +3 -3
- readme.txt +4 -1
docs/Changes.txt
CHANGED
@@ -32,6 +32,9 @@ All script, style and body tags are stripped from html emails.
|
|
32 |
Attachments are now processed in the order they were attached.
|
33 |
|
34 |
== CHANGELOG ==
|
|
|
|
|
|
|
35 |
= 1.8.13 (2016-10-31) =
|
36 |
* Fix bug where inline images were not being found due to case differences, e.g. img_4180.jpg vs. IMG_4180.JPG
|
37 |
|
32 |
Attachments are now processed in the order they were attached.
|
33 |
|
34 |
== CHANGELOG ==
|
35 |
+
= 1.8.14 (2016-11-01) =
|
36 |
+
* Fix bug where OS detection failure was preventing email processing
|
37 |
+
|
38 |
= 1.8.13 (2016-10-31) =
|
39 |
* Fix bug where inline images were not being found due to case differences, e.g. img_4180.jpg vs. IMG_4180.JPG
|
40 |
|
docs/Postie.txt
CHANGED
@@ -6,7 +6,7 @@ Plugin URI: http://PostiePlugin.com/
|
|
6 |
Tags: e-mail, email, post-by-email
|
7 |
Requires at least: 3.3.0
|
8 |
Tested up to: 4.6.1
|
9 |
-
Stable tag: 1.8.
|
10 |
License: GPLv2 or later
|
11 |
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
12 |
|
6 |
Tags: e-mail, email, post-by-email
|
7 |
Requires at least: 3.3.0
|
8 |
Tested up to: 4.6.1
|
9 |
+
Stable tag: 1.8.14
|
10 |
License: GPLv2 or later
|
11 |
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
12 |
|
lib/fCore.php
CHANGED
@@ -424,12 +424,11 @@ class fCore {
|
|
424 |
$valid_oses = array('linux', 'aix', 'bsd', 'freebsd', 'openbsd', 'netbsd', 'osx', 'solaris', 'windows');
|
425 |
|
426 |
if ($invalid_oses = array_diff($oses, $valid_oses)) {
|
427 |
-
throw new fProgrammerException(
|
428 |
-
'One or more of the OSes specified, %$1s, is invalid. Must be one of: %2$s.', join(' ', $invalid_oses), join(', ', $valid_oses)
|
429 |
-
);
|
430 |
}
|
431 |
|
432 |
$uname = php_uname('s');
|
|
|
433 |
|
434 |
if (stripos($uname, 'linux') !== FALSE) {
|
435 |
return in_array('linux', $oses);
|
@@ -449,7 +448,8 @@ class fCore {
|
|
449 |
return in_array('osx', $oses);
|
450 |
}
|
451 |
|
452 |
-
throw new fEnvironmentException(
|
|
|
453 |
}
|
454 |
|
455 |
/**
|
@@ -461,9 +461,7 @@ class fCore {
|
|
461 |
static $running_version = NULL;
|
462 |
|
463 |
if ($running_version === NULL) {
|
464 |
-
$running_version = preg_replace(
|
465 |
-
'#^(\d+\.\d+\.\d+).*$#D', '\1', PHP_VERSION
|
466 |
-
);
|
467 |
}
|
468 |
|
469 |
return version_compare($running_version, $version, '>=');
|
@@ -481,9 +479,7 @@ class fCore {
|
|
481 |
$args = array_slice(func_get_args(), 1);
|
482 |
|
483 |
if (class_exists('fText', FALSE)) {
|
484 |
-
return call_user_func_array(
|
485 |
-
array('fText', 'compose'), array($message, $args)
|
486 |
-
);
|
487 |
} else {
|
488 |
return vsprintf($message, $args);
|
489 |
}
|
@@ -626,9 +622,7 @@ class fCore {
|
|
626 |
*/
|
627 |
static public function enableDynamicConstants() {
|
628 |
if (!self::$handles_errors) {
|
629 |
-
throw new fProgrammerException(
|
630 |
-
'Dynamic constants can not be enabled unless error handling has been enabled via %s', __CLASS__ . '::enableErrorHandling()'
|
631 |
-
);
|
632 |
}
|
633 |
self::$dynamic_constants = TRUE;
|
634 |
}
|
@@ -948,9 +942,7 @@ class fCore {
|
|
948 |
self::call(self::$exception_handler_callback, self::$exception_handler_parameters);
|
949 |
} catch (Exception $e) {
|
950 |
trigger_error(
|
951 |
-
self::compose(
|
952 |
-
'An exception was thrown in the %s closing code callback', 'setExceptionHandling()'
|
953 |
-
), E_USER_ERROR
|
954 |
);
|
955 |
}
|
956 |
}
|
@@ -1042,9 +1034,7 @@ class fCore {
|
|
1042 |
'#^.*[/\\\\](.*)$#', '\1', reset(self::$significant_error_lines)
|
1043 |
);
|
1044 |
|
1045 |
-
$subject = self::compose(
|
1046 |
-
'[%1$s] %2$s error(s) beginning at %3$s {%4$s}', isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : php_uname('n'), count($messages), $first_file_line, $hash
|
1047 |
-
);
|
1048 |
|
1049 |
foreach ($messages as $destination => $message) {
|
1050 |
if (self::$show_context) {
|
424 |
$valid_oses = array('linux', 'aix', 'bsd', 'freebsd', 'openbsd', 'netbsd', 'osx', 'solaris', 'windows');
|
425 |
|
426 |
if ($invalid_oses = array_diff($oses, $valid_oses)) {
|
427 |
+
throw new fProgrammerException('One or more of the OSes specified, %$1s, is invalid. Must be one of: %2$s.', join(' ', $invalid_oses), join(', ', $valid_oses));
|
|
|
|
|
428 |
}
|
429 |
|
430 |
$uname = php_uname('s');
|
431 |
+
DebugEcho("checkOS: $uname");
|
432 |
|
433 |
if (stripos($uname, 'linux') !== FALSE) {
|
434 |
return in_array('linux', $oses);
|
448 |
return in_array('osx', $oses);
|
449 |
}
|
450 |
|
451 |
+
//throw new fEnvironmentException("Unable to determine the current OS ($uname)");
|
452 |
+
return false;
|
453 |
}
|
454 |
|
455 |
/**
|
461 |
static $running_version = NULL;
|
462 |
|
463 |
if ($running_version === NULL) {
|
464 |
+
$running_version = preg_replace('#^(\d+\.\d+\.\d+).*$#D', '\1', PHP_VERSION);
|
|
|
|
|
465 |
}
|
466 |
|
467 |
return version_compare($running_version, $version, '>=');
|
479 |
$args = array_slice(func_get_args(), 1);
|
480 |
|
481 |
if (class_exists('fText', FALSE)) {
|
482 |
+
return call_user_func_array(array('fText', 'compose'), array($message, $args));
|
|
|
|
|
483 |
} else {
|
484 |
return vsprintf($message, $args);
|
485 |
}
|
622 |
*/
|
623 |
static public function enableDynamicConstants() {
|
624 |
if (!self::$handles_errors) {
|
625 |
+
throw new fProgrammerException('Dynamic constants can not be enabled unless error handling has been enabled via %s', __CLASS__ . '::enableErrorHandling()');
|
|
|
|
|
626 |
}
|
627 |
self::$dynamic_constants = TRUE;
|
628 |
}
|
942 |
self::call(self::$exception_handler_callback, self::$exception_handler_parameters);
|
943 |
} catch (Exception $e) {
|
944 |
trigger_error(
|
945 |
+
self::compose('An exception was thrown in the %s closing code callback', 'setExceptionHandling()'), E_USER_ERROR
|
|
|
|
|
946 |
);
|
947 |
}
|
948 |
}
|
1034 |
'#^.*[/\\\\](.*)$#', '\1', reset(self::$significant_error_lines)
|
1035 |
);
|
1036 |
|
1037 |
+
$subject = self::compose('[%1$s] %2$s error(s) beginning at %3$s {%4$s}', isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : php_uname('n'), count($messages), $first_file_line, $hash);
|
|
|
|
|
1038 |
|
1039 |
foreach ($messages as $destination => $message) {
|
1040 |
if (self::$show_context) {
|
lib/fEmail.php
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<?php
|
|
|
2 |
/**
|
3 |
* Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
|
4 |
*
|
@@ -50,28 +51,27 @@
|
|
50 |
* @changes 1.0.0b2 Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
|
51 |
* @changes 1.0.0b The initial implementation [wb, 2008-06-23]
|
52 |
*/
|
53 |
-
class fEmail
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
const EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
|
75 |
(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
|
76 |
(?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
|
77 |
)@( # The @ symbol
|
@@ -79,20 +79,20 @@ class fEmail
|
|
79 |
\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
|
80 |
)[ \t]*$~ixD'; # Allow Trailing whitespace
|
81 |
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*) # An "atom" or a quoted string
|
97 |
(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*) # Another "atom" or a quoted string or a . followed by one of those, any number of times
|
98 |
[ \t]*<[ \t]*(( # The < encapsulating the email address
|
@@ -103,1685 +103,1561 @@ class fEmail
|
|
103 |
\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
|
104 |
))[ \t]*>[ \t]*$~ixD'; # Closing > and trailing whitespace
|
105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
-
/**
|
108 |
-
* Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
|
109 |
-
*
|
110 |
-
* @var boolean
|
111 |
-
*/
|
112 |
-
static private $convert_crlf = FALSE;
|
113 |
-
|
114 |
-
/**
|
115 |
-
* The local fully-qualified domain name
|
116 |
-
*/
|
117 |
-
static private $fqdn;
|
118 |
-
|
119 |
-
/**
|
120 |
-
* Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
|
121 |
-
*
|
122 |
-
* @var boolean
|
123 |
-
*/
|
124 |
-
static private $popen_sendmail = FALSE;
|
125 |
-
|
126 |
-
|
127 |
-
/**
|
128 |
-
* Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
|
129 |
-
*
|
130 |
-
* This method will remove newline characters from the name and email, and
|
131 |
-
* will remove any backslash (`\`) and double quote (`"`) characters from
|
132 |
-
* the name.
|
133 |
-
*
|
134 |
-
* @internal
|
135 |
-
*
|
136 |
-
* @param string $name The name associated with the email address
|
137 |
-
* @param string $email The email address
|
138 |
-
* @return string The '"name" <email>' or 'email' string
|
139 |
-
*/
|
140 |
-
static public function combineNameEmail($name, $email)
|
141 |
-
{
|
142 |
-
// Strip lower ascii character since they aren't useful in email addresses
|
143 |
-
$email = preg_replace('#[\x0-\x19]+#', '', $email);
|
144 |
-
$name = preg_replace('#[\x0-\x19]+#', '', $name);
|
145 |
-
|
146 |
-
if (!$name) {
|
147 |
-
return $email;
|
148 |
-
}
|
149 |
-
|
150 |
-
// If the name contains any non-ascii bytes or stuff not allowed
|
151 |
-
// in quoted strings we just make an encoded word out of it
|
152 |
-
if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
|
153 |
-
// The longest header name that will contain email addresses is
|
154 |
-
// "Bcc: ", which is 5 characters long
|
155 |
-
$name = self::makeEncodedWord($name, 5);
|
156 |
-
} else {
|
157 |
-
$name = '"' . $name . '"';
|
158 |
-
}
|
159 |
-
|
160 |
-
return $name . ' <' . $email . '>';
|
161 |
-
}
|
162 |
-
|
163 |
-
|
164 |
-
/**
|
165 |
-
* Composes text using fText if loaded
|
166 |
-
*
|
167 |
-
* @param string $message The message to compose
|
168 |
-
* @param mixed $component A string or number to insert into the message
|
169 |
-
* @param mixed ...
|
170 |
-
* @return string The composed and possible translated message
|
171 |
-
*/
|
172 |
-
static protected function compose($message)
|
173 |
-
{
|
174 |
-
$args = array_slice(func_get_args(), 1);
|
175 |
-
|
176 |
-
if (class_exists('fText', FALSE)) {
|
177 |
-
return call_user_func_array(
|
178 |
-
array('fText', 'compose'),
|
179 |
-
array($message, $args)
|
180 |
-
);
|
181 |
-
} else {
|
182 |
-
return vsprintf($message, $args);
|
183 |
-
}
|
184 |
-
}
|
185 |
-
|
186 |
-
|
187 |
-
/**
|
188 |
-
* Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
|
189 |
-
*
|
190 |
-
* Before trying to fix qmail with this method, please try using fSMTP
|
191 |
-
* to connect to `localhost` and pass the fSMTP object to ::send().
|
192 |
-
*
|
193 |
-
* @return void
|
194 |
-
*/
|
195 |
-
static public function fixQmail()
|
196 |
-
{
|
197 |
-
if (fCore::checkOS('windows')) {
|
198 |
-
return;
|
199 |
-
}
|
200 |
-
|
201 |
-
$sendmail_command = ini_get('sendmail_path');
|
202 |
-
|
203 |
-
if (!$sendmail_command) {
|
204 |
-
self::$convert_crlf = TRUE;
|
205 |
-
trigger_error(
|
206 |
-
self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'),
|
207 |
-
E_USER_WARNING
|
208 |
-
);
|
209 |
-
trigger_error(
|
210 |
-
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
|
211 |
-
E_USER_WARNING
|
212 |
-
);
|
213 |
-
}
|
214 |
-
|
215 |
-
$sendmail_command_parts = explode(' ', $sendmail_command, 2);
|
216 |
-
|
217 |
-
$sendmail_path = $sendmail_command_parts[0];
|
218 |
-
$sendmail_dir = pathinfo($sendmail_path, PATHINFO_DIRNAME);
|
219 |
-
$sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
|
220 |
-
|
221 |
-
// Check to see if we can run sendmail via popen
|
222 |
-
$executable = FALSE;
|
223 |
-
$safe_mode = FALSE;
|
224 |
-
|
225 |
-
if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
|
226 |
-
$safe_mode = TRUE;
|
227 |
-
$exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
|
228 |
-
foreach ($exec_dirs as $exec_dir) {
|
229 |
-
if (stripos($sendmail_dir, $exec_dir) !== 0) {
|
230 |
-
continue;
|
231 |
-
}
|
232 |
-
if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
|
233 |
-
$executable = TRUE;
|
234 |
-
}
|
235 |
-
}
|
236 |
-
|
237 |
-
} else {
|
238 |
-
if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
|
239 |
-
$executable = TRUE;
|
240 |
-
}
|
241 |
-
}
|
242 |
-
|
243 |
-
if ($executable) {
|
244 |
-
self::$popen_sendmail = TRUE;
|
245 |
-
} else {
|
246 |
-
self::$convert_crlf = TRUE;
|
247 |
-
if ($safe_mode) {
|
248 |
-
trigger_error(
|
249 |
-
self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'),
|
250 |
-
E_USER_WARNING
|
251 |
-
);
|
252 |
-
trigger_error(
|
253 |
-
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
|
254 |
-
E_USER_WARNING
|
255 |
-
);
|
256 |
-
} else {
|
257 |
-
trigger_error(
|
258 |
-
self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'),
|
259 |
-
E_USER_WARNING
|
260 |
-
);
|
261 |
-
trigger_error(
|
262 |
-
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
|
263 |
-
E_USER_WARNING
|
264 |
-
);
|
265 |
-
}
|
266 |
-
}
|
267 |
-
}
|
268 |
-
|
269 |
-
|
270 |
-
/**
|
271 |
-
* Returns the fully-qualified domain name of the server
|
272 |
-
*
|
273 |
-
* @internal
|
274 |
-
*
|
275 |
-
* @return string The fully-qualified domain name of the server
|
276 |
-
*/
|
277 |
-
static public function getFQDN()
|
278 |
-
{
|
279 |
-
if (self::$fqdn !== NULL) {
|
280 |
-
return self::$fqdn;
|
281 |
-
}
|
282 |
-
|
283 |
-
if (isset($_ENV['HOST'])) {
|
284 |
-
self::$fqdn = $_ENV['HOST'];
|
285 |
-
}
|
286 |
-
if (strpos(self::$fqdn, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
|
287 |
-
self::$fqdn = $_ENV['HOSTNAME'];
|
288 |
-
}
|
289 |
-
if (strpos(self::$fqdn, '.') === FALSE) {
|
290 |
-
self::$fqdn = php_uname('n');
|
291 |
-
}
|
292 |
-
|
293 |
-
if (strpos(self::$fqdn, '.') === FALSE) {
|
294 |
-
|
295 |
-
$can_exec = !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode');
|
296 |
-
if (fCore::checkOS('linux') && $can_exec) {
|
297 |
-
self::$fqdn = trim(shell_exec('hostname --fqdn'));
|
298 |
-
|
299 |
-
} elseif (fCore::checkOS('windows')) {
|
300 |
-
$shell = new COM('WScript.Shell');
|
301 |
-
$tcpip_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip';
|
302 |
-
try {
|
303 |
-
$domain = $shell->RegRead($tcpip_key . '\Parameters\NV Domain');
|
304 |
-
} catch (com_exception $e) {
|
305 |
-
try {
|
306 |
-
$domain = $shell->RegRead($tcpip_key . '\Parameters\DhcpDomain');
|
307 |
-
} catch (com_exception $e) {
|
308 |
-
try {
|
309 |
-
$adapters = $shell->RegRead($tcpip_key . '\Linkage\Route');
|
310 |
-
foreach ($adapters as $adapter) {
|
311 |
-
if ($adapter[0] != '{') { continue; }
|
312 |
-
try {
|
313 |
-
$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\Domain');
|
314 |
-
} catch (com_exception $e) {
|
315 |
-
try {
|
316 |
-
$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\DhcpDomain');
|
317 |
-
} catch (com_exception $e) { }
|
318 |
-
}
|
319 |
-
}
|
320 |
-
} catch (com_exception $e) { }
|
321 |
-
}
|
322 |
-
}
|
323 |
-
if (!empty($domain)) {
|
324 |
-
self::$fqdn .= '.' . $domain;
|
325 |
-
}
|
326 |
-
|
327 |
-
} elseif (!fCore::checkOS('windows') && !ini_get('open_basedir') && file_exists('/etc/resolv.conf')) {
|
328 |
-
$output = file_get_contents('/etc/resolv.conf');
|
329 |
-
if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
|
330 |
-
self::$fqdn .= '.' . $match[1];
|
331 |
-
}
|
332 |
-
}
|
333 |
-
}
|
334 |
-
|
335 |
-
return self::$fqdn;
|
336 |
-
}
|
337 |
-
|
338 |
-
|
339 |
-
/**
|
340 |
-
* Encodes a string to UTF-8 encoded-word
|
341 |
-
*
|
342 |
-
* @param string $content The content to encode
|
343 |
-
* @param integer $first_line_prefix_length The length of any prefix applied to the first line of the encoded word - this allows properly accounting for a header name
|
344 |
-
* @return string The encoded string
|
345 |
-
*/
|
346 |
-
static private function makeEncodedWord($content, $first_line_prefix_length)
|
347 |
-
{
|
348 |
-
// Homogenize the line-endings to CRLF
|
349 |
-
$content = str_replace("\r\n", "\n", $content);
|
350 |
-
$content = str_replace("\r", "\n", $content);
|
351 |
-
$content = str_replace("\n", "\r\n", $content);
|
352 |
-
|
353 |
-
// Encoded word is not required if all characters are ascii
|
354 |
-
if (!preg_match('#[\x80-\xFF]#', $content)) {
|
355 |
-
return $content;
|
356 |
-
}
|
357 |
-
|
358 |
-
// A quick a dirty hex encoding
|
359 |
-
$content = rawurlencode($content);
|
360 |
-
$content = str_replace('=', '%3D', $content);
|
361 |
-
$content = str_replace('%', '=', $content);
|
362 |
-
|
363 |
-
// Decode characters that don't have to be coded
|
364 |
-
$decodings = array(
|
365 |
-
'=20' => '_', '=21' => '!', '=22' => '"', '=23' => '#',
|
366 |
-
'=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
|
367 |
-
'=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
|
368 |
-
'=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
|
369 |
-
'=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
|
370 |
-
'=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
|
371 |
-
'=5E' => '^', '=60' => '`', '=7B' => '{', '=7C' => '|',
|
372 |
-
'=7D' => '}', '=7E' => '~', ' ' => '_'
|
373 |
-
);
|
374 |
-
|
375 |
-
$content = strtr($content, $decodings);
|
376 |
-
|
377 |
-
$length = strlen($content);
|
378 |
-
|
379 |
-
$prefix = '=?utf-8?Q?';
|
380 |
-
$suffix = '?=';
|
381 |
-
|
382 |
-
$prefix_length = 10;
|
383 |
-
$suffix_length = 2;
|
384 |
-
|
385 |
-
// This loop goes through and ensures we are wrapping by 75 chars
|
386 |
-
// including the encoded word delimiters
|
387 |
-
$output = $prefix;
|
388 |
-
$line_length = $prefix_length + $first_line_prefix_length;
|
389 |
-
|
390 |
-
for ($i=0; $i<$length; $i++) {
|
391 |
-
|
392 |
-
// Get info about the next character
|
393 |
-
$char_length = ($content[$i] == '=') ? 3 : 1;
|
394 |
-
$char = $content[$i];
|
395 |
-
if ($char_length == 3) {
|
396 |
-
$char .= $content[$i+1] . $content[$i+2];
|
397 |
-
}
|
398 |
-
|
399 |
-
// If we have too long a line, wrap it
|
400 |
-
if ($line_length + $suffix_length + $char_length > 75) {
|
401 |
-
$output .= $suffix . "\r\n " . $prefix;
|
402 |
-
$line_length = $prefix_length + 2;
|
403 |
-
}
|
404 |
-
|
405 |
-
// Add the character
|
406 |
-
$output .= $char;
|
407 |
-
|
408 |
-
// Figure out how much longer the line is
|
409 |
-
$line_length += $char_length;
|
410 |
-
|
411 |
-
// Skip characters if we have an encoded character
|
412 |
-
$i += $char_length-1;
|
413 |
-
}
|
414 |
-
|
415 |
-
if (substr($output, -2) != $suffix) {
|
416 |
-
$output .= $suffix;
|
417 |
-
}
|
418 |
-
|
419 |
-
return $output;
|
420 |
-
}
|
421 |
-
|
422 |
-
|
423 |
-
/**
|
424 |
-
* Resets the configuration of the class
|
425 |
-
*
|
426 |
-
* @internal
|
427 |
-
*
|
428 |
-
* @return void
|
429 |
-
*/
|
430 |
-
static public function reset()
|
431 |
-
{
|
432 |
-
self::$convert_crlf = FALSE;
|
433 |
-
self::$fqdn = NULL;
|
434 |
-
self::$popen_sendmail = FALSE;
|
435 |
-
}
|
436 |
-
|
437 |
-
|
438 |
-
/**
|
439 |
-
* Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
|
440 |
-
*
|
441 |
-
* @param mixed $value The value to check
|
442 |
-
* @return boolean If the value is string-like
|
443 |
-
*/
|
444 |
-
static protected function stringlike($value)
|
445 |
-
{
|
446 |
-
if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
|
447 |
-
return FALSE;
|
448 |
-
}
|
449 |
-
|
450 |
-
return TRUE;
|
451 |
-
}
|
452 |
-
|
453 |
-
|
454 |
-
/**
|
455 |
-
* Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
|
456 |
-
*
|
457 |
-
* @param string $text The text to unindent and replace constants in
|
458 |
-
* @return string The unindented text
|
459 |
-
*/
|
460 |
-
static public function unindentExpand($text)
|
461 |
-
{
|
462 |
-
$text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
|
463 |
-
|
464 |
-
if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
|
465 |
-
$text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
|
466 |
-
}
|
467 |
-
|
468 |
-
preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
|
469 |
-
foreach ($constants as $constant) {
|
470 |
-
if (!defined($constant[1])) { continue; }
|
471 |
-
$text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
|
472 |
-
}
|
473 |
-
|
474 |
-
return $text;
|
475 |
-
}
|
476 |
-
|
477 |
-
|
478 |
-
/**
|
479 |
-
* The file contents to attach
|
480 |
-
*
|
481 |
-
* @var array
|
482 |
-
*/
|
483 |
-
private $attachments = array();
|
484 |
-
|
485 |
-
/**
|
486 |
-
* The email address(es) to BCC to
|
487 |
-
*
|
488 |
-
* @var array
|
489 |
-
*/
|
490 |
-
private $bcc_emails = array();
|
491 |
-
|
492 |
-
/**
|
493 |
-
* The email address to bounce to
|
494 |
-
*
|
495 |
-
* @var string
|
496 |
-
*/
|
497 |
-
private $bounce_to_email = NULL;
|
498 |
-
|
499 |
-
/**
|
500 |
-
* The email address(es) to CC to
|
501 |
-
*
|
502 |
-
* @var array
|
503 |
-
*/
|
504 |
-
private $cc_emails = array();
|
505 |
-
|
506 |
-
/**
|
507 |
-
* Custom headers
|
508 |
-
*
|
509 |
-
* @var array
|
510 |
-
*/
|
511 |
-
private $custom_headers = array();
|
512 |
-
|
513 |
-
/**
|
514 |
-
* The email address being sent from
|
515 |
-
*
|
516 |
-
* @var string
|
517 |
-
*/
|
518 |
-
private $from_email = NULL;
|
519 |
-
|
520 |
-
/**
|
521 |
-
* The HTML body of the email
|
522 |
-
*
|
523 |
-
* @var string
|
524 |
-
*/
|
525 |
-
private $html_body = NULL;
|
526 |
-
|
527 |
-
/**
|
528 |
-
* The Message-ID header for the email
|
529 |
-
*
|
530 |
-
* @var string
|
531 |
-
*/
|
532 |
-
private $message_id = NULL;
|
533 |
-
|
534 |
-
/**
|
535 |
-
* The plaintext body of the email
|
536 |
-
*
|
537 |
-
* @var string
|
538 |
-
*/
|
539 |
-
private $plaintext_body = NULL;
|
540 |
-
|
541 |
-
/**
|
542 |
-
* The recipient's S/MIME PEM certificate filename, used for encryption of the message
|
543 |
-
*
|
544 |
-
* @var string
|
545 |
-
*/
|
546 |
-
private $recipients_smime_cert_file = NULL;
|
547 |
-
|
548 |
-
/**
|
549 |
-
* The files to include as multipart/related
|
550 |
-
*
|
551 |
-
* @var array
|
552 |
-
*/
|
553 |
-
private $related_files = array();
|
554 |
-
|
555 |
-
/**
|
556 |
-
* The email address to reply to
|
557 |
-
*
|
558 |
-
* @var string
|
559 |
-
*/
|
560 |
-
private $reply_to_email = NULL;
|
561 |
-
|
562 |
-
/**
|
563 |
-
* The email address actually sending the email
|
564 |
-
*
|
565 |
-
* @var string
|
566 |
-
*/
|
567 |
-
private $sender_email = NULL;
|
568 |
-
|
569 |
-
/**
|
570 |
-
* The senders's S/MIME PEM certificate filename, used for singing the message
|
571 |
-
*
|
572 |
-
* @var string
|
573 |
-
*/
|
574 |
-
private $senders_smime_cert_file = NULL;
|
575 |
-
|
576 |
-
/**
|
577 |
-
* The senders's S/MIME private key filename, used for singing the message
|
578 |
-
*
|
579 |
-
* @var string
|
580 |
-
*/
|
581 |
-
private $senders_smime_pk_file = NULL;
|
582 |
-
|
583 |
-
/**
|
584 |
-
* The senders's S/MIME private key password, used for singing the message
|
585 |
-
*
|
586 |
-
* @var string
|
587 |
-
*/
|
588 |
-
private $senders_smime_pk_password = NULL;
|
589 |
-
|
590 |
-
/**
|
591 |
-
* If the message should be encrypted using the recipient's S/MIME certificate
|
592 |
-
*
|
593 |
-
* @var boolean
|
594 |
-
*/
|
595 |
-
private $smime_encrypt = FALSE;
|
596 |
-
|
597 |
-
/**
|
598 |
-
* If the message should be signed using the senders's S/MIME private key
|
599 |
-
*
|
600 |
-
* @var boolean
|
601 |
-
*/
|
602 |
-
private $smime_sign = FALSE;
|
603 |
-
|
604 |
-
/**
|
605 |
-
* The subject of the email
|
606 |
-
*
|
607 |
-
* @var string
|
608 |
-
*/
|
609 |
-
private $subject = NULL;
|
610 |
-
|
611 |
-
/**
|
612 |
-
* The email address(es) to send to
|
613 |
-
*
|
614 |
-
* @var array
|
615 |
-
*/
|
616 |
-
private $to_emails = array();
|
617 |
-
|
618 |
-
|
619 |
-
/**
|
620 |
-
* Initializes fEmail for creating message ids
|
621 |
-
*
|
622 |
-
* @return fEmail
|
623 |
-
*/
|
624 |
-
public function __construct()
|
625 |
-
{
|
626 |
-
$this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
|
627 |
-
}
|
628 |
-
|
629 |
-
|
630 |
-
/**
|
631 |
-
* All requests that hit this method should be requests for callbacks
|
632 |
-
*
|
633 |
-
* @internal
|
634 |
-
*
|
635 |
-
* @param string $method The method to create a callback for
|
636 |
-
* @return callback The callback for the method requested
|
637 |
-
*/
|
638 |
-
public function __get($method)
|
639 |
-
{
|
640 |
-
return array($this, $method);
|
641 |
-
}
|
642 |
-
|
643 |
-
|
644 |
-
/**
|
645 |
-
* Adds an attachment to the email
|
646 |
-
*
|
647 |
-
* If a duplicate filename is detected, it will be changed to be unique.
|
648 |
-
*
|
649 |
-
* @param string|fFile $contents The contents of the file
|
650 |
-
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
|
651 |
-
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
|
652 |
-
* @return fEmail The email object, to allow for method chaining
|
653 |
-
*/
|
654 |
-
public function addAttachment($contents, $filename=NULL, $mime_type=NULL)
|
655 |
-
{
|
656 |
-
$this->extrapolateFileInfo($contents, $filename, $mime_type);
|
657 |
-
|
658 |
-
while (isset($this->attachments[$filename])) {
|
659 |
-
$filename = $this->generateNewFilename($filename);
|
660 |
-
}
|
661 |
-
|
662 |
-
$this->attachments[$filename] = array(
|
663 |
-
'mime-type' => $mime_type,
|
664 |
-
'contents' => $contents
|
665 |
-
);
|
666 |
-
|
667 |
-
return $this;
|
668 |
-
}
|
669 |
-
|
670 |
-
|
671 |
-
/**
|
672 |
-
* Adds a “related” file to the email, returning the `Content-ID` for use in HTML
|
673 |
-
*
|
674 |
-
* The purpose of a related file is to be able to reference it in part of
|
675 |
-
* the HTML body. Image `src` URLs can reference a related file by starting
|
676 |
-
* the URL with `cid:` and then inserting the `Content-ID`.
|
677 |
-
*
|
678 |
-
* If a duplicate filename is detected, it will be changed to be unique.
|
679 |
-
*
|
680 |
-
* @param string|fFile $contents The contents of the file
|
681 |
-
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
|
682 |
-
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
|
683 |
-
* @return string The fully-formed `cid:` URL for use in HTML `src` attributes
|
684 |
-
*/
|
685 |
-
public function addRelatedFile($contents, $filename=NULL, $mime_type=NULL)
|
686 |
-
{
|
687 |
-
$this->extrapolateFileInfo($contents, $filename, $mime_type);
|
688 |
-
|
689 |
-
while (isset($this->related_files[$filename])) {
|
690 |
-
$filename = $this->generateNewFilename($filename);
|
691 |
-
}
|
692 |
-
|
693 |
-
$cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);
|
694 |
-
|
695 |
-
$this->related_files[$filename] = array(
|
696 |
-
'mime-type' => $mime_type,
|
697 |
-
'contents' => $contents,
|
698 |
-
'content-id' => '<' . $cid . '>'
|
699 |
-
);
|
700 |
-
|
701 |
-
return 'cid:' . $cid;
|
702 |
-
}
|
703 |
-
|
704 |
-
|
705 |
-
/**
|
706 |
-
* Adds a blind carbon copy (BCC) email recipient
|
707 |
-
*
|
708 |
-
* @param string $email The email address to BCC
|
709 |
-
* @param string $name The recipient's name
|
710 |
-
* @return fEmail The email object, to allow for method chaining
|
711 |
-
*/
|
712 |
-
public function addBCCRecipient($email, $name=NULL)
|
713 |
-
{
|
714 |
-
if (!$email) {
|
715 |
-
return;
|
716 |
-
}
|
717 |
-
|
718 |
-
$this->bcc_emails[] = self::combineNameEmail($name, $email);
|
719 |
-
|
720 |
-
return $this;
|
721 |
-
}
|
722 |
-
|
723 |
-
|
724 |
-
/**
|
725 |
-
* Adds a carbon copy (CC) email recipient
|
726 |
-
*
|
727 |
-
* @param string $email The email address to BCC
|
728 |
-
* @param string $name The recipient's name
|
729 |
-
* @return fEmail The email object, to allow for method chaining
|
730 |
-
*/
|
731 |
-
public function addCCRecipient($email, $name=NULL)
|
732 |
-
{
|
733 |
-
if (!$email) {
|
734 |
-
return;
|
735 |
-
}
|
736 |
-
|
737 |
-
$this->cc_emails[] = self::combineNameEmail($name, $email);
|
738 |
-
|
739 |
-
return $this;
|
740 |
-
}
|
741 |
-
|
742 |
-
|
743 |
-
/**
|
744 |
-
* Allows adding a custom header to the email
|
745 |
-
*
|
746 |
-
* If the method is called multiple times with the same name, the last
|
747 |
-
* value will be used.
|
748 |
-
*
|
749 |
-
* Please note that this class will properly format the header, including
|
750 |
-
* adding the `:` between the name and value and wrapping values that are
|
751 |
-
* too long for a single line.
|
752 |
-
*
|
753 |
-
* @param string $name The name of the header
|
754 |
-
* @param string $value The value of the header
|
755 |
-
* @param array :$headers An associative array of `{name} => {value}`
|
756 |
-
* @return fEmail The email object, to allow for method chaining
|
757 |
-
*/
|
758 |
-
public function addCustomHeader($name, $value=NULL)
|
759 |
-
{
|
760 |
-
if ($value === NULL && is_array($name)) {
|
761 |
-
foreach ($name as $key => $value) {
|
762 |
-
$this->addCustomHeader($key, $value);
|
763 |
-
}
|
764 |
-
return;
|
765 |
-
}
|
766 |
-
|
767 |
-
$lower_name = fUTF8::lower($name);
|
768 |
-
$this->custom_headers[$lower_name] = array($name, $value);
|
769 |
-
|
770 |
-
return $this;
|
771 |
-
}
|
772 |
-
|
773 |
-
|
774 |
-
/**
|
775 |
-
* Adds an email recipient
|
776 |
-
*
|
777 |
-
* @param string $email The email address to send to
|
778 |
-
* @param string $name The recipient's name
|
779 |
-
* @return fEmail The email object, to allow for method chaining
|
780 |
-
*/
|
781 |
-
public function addRecipient($email, $name=NULL)
|
782 |
-
{
|
783 |
-
if (!$email) {
|
784 |
-
return;
|
785 |
-
}
|
786 |
-
|
787 |
-
$this->to_emails[] = self::combineNameEmail($name, $email);
|
788 |
-
|
789 |
-
return $this;
|
790 |
-
}
|
791 |
-
|
792 |
-
|
793 |
-
/**
|
794 |
-
* Takes a multi-address email header and builds it out using an array of emails
|
795 |
-
*
|
796 |
-
* @param string $header The header name without `': '`, the header is non-blank, `': '` will be added
|
797 |
-
* @param array $emails The email addresses for the header
|
798 |
-
* @return string The email header with a trailing `\r\n`
|
799 |
-
*/
|
800 |
-
private function buildMultiAddressHeader($header, $emails)
|
801 |
-
{
|
802 |
-
$header .= ': ';
|
803 |
-
|
804 |
-
$first = TRUE;
|
805 |
-
$line = 1;
|
806 |
-
foreach ($emails as $email) {
|
807 |
-
if ($first) { $first = FALSE; } else { $header .= ', '; }
|
808 |
-
|
809 |
-
// Try to stay within the recommended 78 character line limit
|
810 |
-
$last_crlf_pos = (integer) strrpos($header, "\r\n");
|
811 |
-
if (strlen($header . $email) - $last_crlf_pos > 78) {
|
812 |
-
$header .= "\r\n ";
|
813 |
-
$line++;
|
814 |
-
}
|
815 |
-
|
816 |
-
$header .= trim($email);
|
817 |
-
}
|
818 |
-
|
819 |
-
return $header . "\r\n";
|
820 |
-
}
|
821 |
-
|
822 |
-
|
823 |
-
/**
|
824 |
-
* Removes all To, CC and BCC recipients from the email
|
825 |
-
*
|
826 |
-
* @return fEmail The email object, to allow for method chaining
|
827 |
-
*/
|
828 |
-
public function clearRecipients()
|
829 |
-
{
|
830 |
-
$this->to_emails = array();
|
831 |
-
$this->cc_emails = array();
|
832 |
-
$this->bcc_emails = array();
|
833 |
-
|
834 |
-
return $this;
|
835 |
-
}
|
836 |
-
|
837 |
-
|
838 |
-
/**
|
839 |
-
* Creates a 32-character boundary for a multipart message
|
840 |
-
*
|
841 |
-
* @return string A multipart boundary
|
842 |
-
*/
|
843 |
-
private function createBoundary()
|
844 |
-
{
|
845 |
-
$chars = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
|
846 |
-
$last_index = strlen($chars) - 1;
|
847 |
-
$output = '';
|
848 |
-
|
849 |
-
for ($i = 0; $i < 28; $i++) {
|
850 |
-
$output .= $chars[rand(0, $last_index)];
|
851 |
-
}
|
852 |
-
return $output;
|
853 |
-
}
|
854 |
-
|
855 |
-
|
856 |
-
/**
|
857 |
-
* Builds the body of the email
|
858 |
-
*
|
859 |
-
* @param string $boundary The boundary to use for the top level mime block
|
860 |
-
* @return string The message body to be sent to the mail() function
|
861 |
-
*/
|
862 |
-
private function createBody($boundary)
|
863 |
-
{
|
864 |
-
$boundary_stack = array($boundary);
|
865 |
-
|
866 |
-
$mime_notice = self::compose(
|
867 |
-
"This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
|
868 |
-
);
|
869 |
-
|
870 |
-
$body = '';
|
871 |
-
|
872 |
-
if ($this->html_body || $this->attachments) {
|
873 |
-
$body .= $mime_notice . "\r\n\r\n";
|
874 |
-
}
|
875 |
-
|
876 |
-
if ($this->html_body && $this->related_files && $this->attachments) {
|
877 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
878 |
-
$boundary_stack[] = $this->createBoundary();
|
879 |
-
$body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
|
880 |
-
}
|
881 |
-
|
882 |
-
if ($this->html_body && ($this->attachments || $this->related_files)) {
|
883 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
884 |
-
$boundary_stack[] = $this->createBoundary();
|
885 |
-
$body .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
|
886 |
-
}
|
887 |
-
|
888 |
-
if ($this->html_body || $this->attachments) {
|
889 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
890 |
-
$body .= "Content-Type: text/plain; charset=utf-8\r\n";
|
891 |
-
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
|
892 |
-
}
|
893 |
-
|
894 |
-
$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
|
895 |
-
|
896 |
-
if ($this->html_body) {
|
897 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
898 |
-
$body .= "Content-Type: text/html; charset=utf-8\r\n";
|
899 |
-
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
|
900 |
-
$body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
|
901 |
-
}
|
902 |
-
|
903 |
-
if ($this->related_files) {
|
904 |
-
$body .= '--' . end($boundary_stack) . "--\r\n";
|
905 |
-
array_pop($boundary_stack);
|
906 |
-
|
907 |
-
foreach ($this->related_files as $filename => $file_info) {
|
908 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
909 |
-
$body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
|
910 |
-
$body .= "Content-Transfer-Encoding: base64\r\n";
|
911 |
-
$body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
|
912 |
-
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
|
913 |
-
}
|
914 |
-
}
|
915 |
-
|
916 |
-
if ($this->attachments) {
|
917 |
-
|
918 |
-
if ($this->html_body) {
|
919 |
-
$body .= '--' . end($boundary_stack) . "--\r\n";
|
920 |
-
array_pop($boundary_stack);
|
921 |
-
}
|
922 |
-
|
923 |
-
foreach ($this->attachments as $filename => $file_info) {
|
924 |
-
$body .= '--' . end($boundary_stack) . "\r\n";
|
925 |
-
$body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
|
926 |
-
$body .= "Content-Transfer-Encoding: base64\r\n";
|
927 |
-
$body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
|
928 |
-
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
|
929 |
-
}
|
930 |
-
}
|
931 |
-
|
932 |
-
if ($this->html_body || $this->attachments) {
|
933 |
-
$body .= '--' . end($boundary_stack) . "--\r\n";
|
934 |
-
array_pop($boundary_stack);
|
935 |
-
}
|
936 |
-
|
937 |
-
return $body;
|
938 |
-
}
|
939 |
-
|
940 |
-
|
941 |
-
/**
|
942 |
-
* Builds the headers for the email
|
943 |
-
*
|
944 |
-
* @param string $boundary The boundary to use for the top level mime block
|
945 |
-
* @param string $message_id The message id for the message
|
946 |
-
* @return string The headers to be sent to the [http://php.net/function.mail mail()] function
|
947 |
-
*/
|
948 |
-
private function createHeaders($boundary, $message_id)
|
949 |
-
{
|
950 |
-
$headers = '';
|
951 |
-
|
952 |
-
if ($this->cc_emails) {
|
953 |
-
$headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
|
954 |
-
}
|
955 |
-
|
956 |
-
if ($this->bcc_emails) {
|
957 |
-
$headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
|
958 |
-
}
|
959 |
-
|
960 |
-
$headers .= "From: " . trim($this->from_email) . "\r\n";
|
961 |
-
|
962 |
-
if ($this->reply_to_email) {
|
963 |
-
$headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
|
964 |
-
}
|
965 |
-
|
966 |
-
if ($this->sender_email) {
|
967 |
-
$headers .= "Sender: " . trim($this->sender_email) . "\r\n";
|
968 |
-
}
|
969 |
-
|
970 |
-
foreach ($this->custom_headers as $header_info) {
|
971 |
-
$header_prefix = $header_info[0] . ': ';
|
972 |
-
$headers .= $header_prefix . self::makeEncodedWord($header_info[1], strlen($header_prefix)) . "\r\n";
|
973 |
-
}
|
974 |
-
|
975 |
-
$headers .= "Message-ID: " . $message_id . "\r\n";
|
976 |
-
$headers .= "MIME-Version: 1.0\r\n";
|
977 |
-
|
978 |
-
if (!$this->html_body && !$this->attachments) {
|
979 |
-
$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
|
980 |
-
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
981 |
-
|
982 |
-
} elseif ($this->html_body && !$this->attachments) {
|
983 |
-
if ($this->related_files) {
|
984 |
-
$headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
|
985 |
-
} else {
|
986 |
-
$headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
|
987 |
-
}
|
988 |
-
|
989 |
-
} elseif ($this->attachments) {
|
990 |
-
$headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
|
991 |
-
}
|
992 |
-
|
993 |
-
return $headers . "\r\n";
|
994 |
-
}
|
995 |
-
|
996 |
-
|
997 |
-
/**
|
998 |
-
* Takes the body of the message and processes it with S/MIME
|
999 |
-
*
|
1000 |
-
* @param string $to The recipients being sent to
|
1001 |
-
* @param string $subject The subject of the email
|
1002 |
-
* @param string $headers The headers for the message
|
1003 |
-
* @param string $body The message body
|
1004 |
-
* @return array `0` => The message headers, `1` => The message body
|
1005 |
-
*/
|
1006 |
-
private function createSMIMEBody($to, $subject, $headers, $body)
|
1007 |
-
{
|
1008 |
-
if (!$this->smime_encrypt && !$this->smime_sign) {
|
1009 |
-
return array($headers, $body);
|
1010 |
-
}
|
1011 |
-
|
1012 |
-
$plaintext_file = tempnam('', '__fEmail_');
|
1013 |
-
$ciphertext_file = tempnam('', '__fEmail_');
|
1014 |
-
|
1015 |
-
$headers_array = array(
|
1016 |
-
'To' => $to,
|
1017 |
-
'Subject' => $subject
|
1018 |
-
);
|
1019 |
-
|
1020 |
-
preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
|
1021 |
-
foreach ($header_matches as $header_match) {
|
1022 |
-
$headers_array[$header_match[1]] = trim($header_match[2]);
|
1023 |
-
}
|
1024 |
-
|
1025 |
-
$body_headers = "";
|
1026 |
-
if (isset($headers_array['Content-Type'])) {
|
1027 |
-
$body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
|
1028 |
-
}
|
1029 |
-
if (isset($headers_array['Content-Transfer-Encoding'])) {
|
1030 |
-
$body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
|
1031 |
-
}
|
1032 |
-
|
1033 |
-
if ($body_headers) {
|
1034 |
-
$body = $body_headers . "\r\n" . $body;
|
1035 |
-
}
|
1036 |
-
|
1037 |
-
file_put_contents($plaintext_file, $body);
|
1038 |
-
file_put_contents($ciphertext_file, '');
|
1039 |
-
|
1040 |
-
// Set up the neccessary S/MIME resources
|
1041 |
-
if ($this->smime_sign) {
|
1042 |
-
$senders_smime_cert = file_get_contents($this->senders_smime_cert_file);
|
1043 |
-
$senders_private_key = openssl_pkey_get_private(
|
1044 |
-
file_get_contents($this->senders_smime_pk_file),
|
1045 |
-
$this->senders_smime_pk_password
|
1046 |
-
);
|
1047 |
-
|
1048 |
-
if ($senders_private_key === FALSE) {
|
1049 |
-
throw new fValidationException(
|
1050 |
-
"The sender's S/MIME private key password specified does not appear to be valid for the private key"
|
1051 |
-
);
|
1052 |
-
}
|
1053 |
-
}
|
1054 |
-
|
1055 |
-
if ($this->smime_encrypt) {
|
1056 |
-
$recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
|
1057 |
-
}
|
1058 |
-
|
1059 |
-
|
1060 |
-
// If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
|
1061 |
-
if ($this->smime_encrypt && $this->smime_sign) {
|
1062 |
-
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
|
1063 |
-
openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
|
1064 |
-
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
|
1065 |
-
|
1066 |
-
} elseif ($this->smime_sign) {
|
1067 |
-
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
|
1068 |
-
|
1069 |
-
} elseif ($this->smime_encrypt) {
|
1070 |
-
openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
|
1071 |
-
}
|
1072 |
-
|
1073 |
-
// It seems that the contents of the ciphertext is not always \r\n line breaks
|
1074 |
-
$message = file_get_contents($ciphertext_file);
|
1075 |
-
$message = str_replace("\r\n", "\n", $message);
|
1076 |
-
$message = str_replace("\r", "\n", $message);
|
1077 |
-
$message = str_replace("\n", "\r\n", $message);
|
1078 |
-
|
1079 |
-
list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
|
1080 |
-
|
1081 |
-
$new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
|
1082 |
-
$new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
|
1083 |
-
$new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
|
1084 |
-
$new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
|
1085 |
-
$new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
|
1086 |
-
|
1087 |
-
unlink($plaintext_file);
|
1088 |
-
unlink($ciphertext_file);
|
1089 |
-
|
1090 |
-
if ($this->smime_sign) {
|
1091 |
-
openssl_pkey_free($senders_private_key);
|
1092 |
-
}
|
1093 |
-
|
1094 |
-
return array($new_headers, $new_body);
|
1095 |
-
}
|
1096 |
-
|
1097 |
-
|
1098 |
-
/**
|
1099 |
-
* Sets the email to be encrypted with S/MIME
|
1100 |
-
*
|
1101 |
-
* @param string $recipients_smime_cert_file The file path to the PEM-encoded S/MIME certificate for the recipient
|
1102 |
-
* @return fEmail The email object, to allow for method chaining
|
1103 |
-
*/
|
1104 |
-
public function encrypt($recipients_smime_cert_file)
|
1105 |
-
{
|
1106 |
-
if (!extension_loaded('openssl')) {
|
1107 |
-
throw new fEnvironmentException(
|
1108 |
-
'S/MIME encryption was requested for an email, but the %s extension is not installed',
|
1109 |
-
'openssl'
|
1110 |
-
);
|
1111 |
-
}
|
1112 |
-
|
1113 |
-
if (!self::stringlike($recipients_smime_cert_file)) {
|
1114 |
-
throw new fProgrammerException(
|
1115 |
-
"The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename",
|
1116 |
-
$recipients_smime_cert_file
|
1117 |
-
);
|
1118 |
-
}
|
1119 |
-
|
1120 |
-
$this->smime_encrypt = TRUE;
|
1121 |
-
$this->recipients_smime_cert_file = $recipients_smime_cert_file;
|
1122 |
-
|
1123 |
-
return $this;
|
1124 |
-
}
|
1125 |
-
|
1126 |
-
|
1127 |
-
/**
|
1128 |
-
* Extracts just the email addresses from an array of strings containing an
|
1129 |
-
* <email@address.com> or "Name" <email@address.com> combination.
|
1130 |
-
*
|
1131 |
-
* @param array $list The list of email or name/email to extract from
|
1132 |
-
* @return array The email addresses
|
1133 |
-
*/
|
1134 |
-
private function extractEmails($list)
|
1135 |
-
{
|
1136 |
-
$output = array();
|
1137 |
-
foreach ($list as $email) {
|
1138 |
-
if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
|
1139 |
-
$output[] = $match[2];
|
1140 |
-
} else {
|
1141 |
-
preg_match(self::EMAIL_REGEX, $email, $match);
|
1142 |
-
$output[] = $match[0];
|
1143 |
-
}
|
1144 |
-
}
|
1145 |
-
return $output;
|
1146 |
-
}
|
1147 |
-
|
1148 |
-
|
1149 |
-
/**
|
1150 |
-
* Extracts the filename and mime-type from an fFile object
|
1151 |
-
*
|
1152 |
-
* @param string|fFile &$contents The file to extrapolate the info from
|
1153 |
-
* @param string &$filename The filename to use for the file
|
1154 |
-
* @param string &$mime_type The mime type of the file
|
1155 |
-
* @return void
|
1156 |
-
*/
|
1157 |
-
private function extrapolateFileInfo(&$contents, &$filename, &$mime_type)
|
1158 |
-
{
|
1159 |
-
if ($contents instanceof fFile) {
|
1160 |
-
if ($filename === NULL) {
|
1161 |
-
$filename = $contents->getName();
|
1162 |
-
}
|
1163 |
-
if ($mime_type === NULL) {
|
1164 |
-
$mime_type = $contents->getMimeType();
|
1165 |
-
}
|
1166 |
-
$contents = $contents->read();
|
1167 |
-
|
1168 |
-
} else {
|
1169 |
-
if (!self::stringlike($filename)) {
|
1170 |
-
throw new fProgrammerException(
|
1171 |
-
'The filename specified, %s, does not appear to be a valid filename',
|
1172 |
-
$filename
|
1173 |
-
);
|
1174 |
-
}
|
1175 |
-
|
1176 |
-
$filename = (string) $filename;
|
1177 |
-
|
1178 |
-
if ($mime_type === NULL) {
|
1179 |
-
$mime_type = fFile::determineMimeType($filename, $contents);
|
1180 |
-
}
|
1181 |
-
}
|
1182 |
-
}
|
1183 |
-
|
1184 |
-
|
1185 |
-
/**
|
1186 |
-
* Generates a new filename in an attempt to create a unique name
|
1187 |
-
*
|
1188 |
-
* @param string $filename The filename to generate another name for
|
1189 |
-
* @return string The newly generated filename
|
1190 |
-
*/
|
1191 |
-
private function generateNewFilename($filename)
|
1192 |
-
{
|
1193 |
-
$filename_info = fFilesystem::getPathInfo($filename);
|
1194 |
-
if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
|
1195 |
-
$i = $match[1] + 1;
|
1196 |
-
} else {
|
1197 |
-
$i = 1;
|
1198 |
-
}
|
1199 |
-
$extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
|
1200 |
-
return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
|
1201 |
-
}
|
1202 |
-
|
1203 |
-
|
1204 |
-
/**
|
1205 |
-
* Loads the plaintext version of the email body from a file and applies replacements
|
1206 |
-
*
|
1207 |
-
* The should contain either ASCII or UTF-8 encoded text. Please see
|
1208 |
-
* http://flourishlib.com/docs/UTF-8 for more information.
|
1209 |
-
*
|
1210 |
-
* @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
|
1211 |
-
*
|
1212 |
-
* @param string|fFile $file The plaintext version of the email body
|
1213 |
-
* @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
|
1214 |
-
* @return fEmail The email object, to allow for method chaining
|
1215 |
-
*/
|
1216 |
-
public function loadBody($file, $replacements=array())
|
1217 |
-
{
|
1218 |
-
if (!$file instanceof fFile) {
|
1219 |
-
$file = new fFile($file);
|
1220 |
-
}
|
1221 |
-
|
1222 |
-
$plaintext = $file->read();
|
1223 |
-
if ($replacements) {
|
1224 |
-
$plaintext = strtr($plaintext, $replacements);
|
1225 |
-
}
|
1226 |
-
|
1227 |
-
$this->plaintext_body = $plaintext;
|
1228 |
-
|
1229 |
-
return $this;
|
1230 |
-
}
|
1231 |
-
|
1232 |
-
|
1233 |
-
/**
|
1234 |
-
* Loads the plaintext version of the email body from a file and applies replacements
|
1235 |
-
*
|
1236 |
-
* The should contain either ASCII or UTF-8 encoded text. Please see
|
1237 |
-
* http://flourishlib.com/docs/UTF-8 for more information.
|
1238 |
-
*
|
1239 |
-
* @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
|
1240 |
-
*
|
1241 |
-
* @param string|fFile $file The plaintext version of the email body
|
1242 |
-
* @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
|
1243 |
-
* @return fEmail The email object, to allow for method chaining
|
1244 |
-
*/
|
1245 |
-
public function loadHTMLBody($file, $replacements=array())
|
1246 |
-
{
|
1247 |
-
if (!$file instanceof fFile) {
|
1248 |
-
$file = new fFile($file);
|
1249 |
-
}
|
1250 |
-
|
1251 |
-
$html = $file->read();
|
1252 |
-
if ($replacements) {
|
1253 |
-
$html = strtr($html, $replacements);
|
1254 |
-
}
|
1255 |
-
|
1256 |
-
$this->html_body = $html;
|
1257 |
-
|
1258 |
-
return $this;
|
1259 |
-
}
|
1260 |
-
|
1261 |
-
|
1262 |
-
/**
|
1263 |
-
* Encodes a string to base64
|
1264 |
-
*
|
1265 |
-
* @param string $content The content to encode
|
1266 |
-
* @return string The encoded string
|
1267 |
-
*/
|
1268 |
-
private function makeBase64($content)
|
1269 |
-
{
|
1270 |
-
return chunk_split(base64_encode($content));
|
1271 |
-
}
|
1272 |
-
|
1273 |
-
|
1274 |
-
/**
|
1275 |
-
* Encodes a string to quoted-printable, properly handles UTF-8
|
1276 |
-
*
|
1277 |
-
* @param string $content The content to encode
|
1278 |
-
* @return string The encoded string
|
1279 |
-
*/
|
1280 |
-
private function makeQuotedPrintable($content)
|
1281 |
-
{
|
1282 |
-
// Homogenize the line-endings to CRLF
|
1283 |
-
$content = str_replace("\r\n", "\n", $content);
|
1284 |
-
$content = str_replace("\r", "\n", $content);
|
1285 |
-
$content = str_replace("\n", "\r\n", $content);
|
1286 |
-
|
1287 |
-
// A quick a dirty hex encoding
|
1288 |
-
$content = rawurlencode($content);
|
1289 |
-
$content = str_replace('=', '%3D', $content);
|
1290 |
-
$content = str_replace('%', '=', $content);
|
1291 |
-
|
1292 |
-
// Decode characters that don't have to be coded
|
1293 |
-
$decodings = array(
|
1294 |
-
'=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
|
1295 |
-
'=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
|
1296 |
-
'=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
|
1297 |
-
'=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
|
1298 |
-
'=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
|
1299 |
-
'=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
|
1300 |
-
'=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
|
1301 |
-
'=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
|
1302 |
-
);
|
1303 |
-
|
1304 |
-
$content = strtr($content, $decodings);
|
1305 |
-
|
1306 |
-
$output = '';
|
1307 |
-
|
1308 |
-
$length = strlen($content);
|
1309 |
-
|
1310 |
-
// This loop goes through and ensures we are wrapping by 76 chars
|
1311 |
-
$line_length = 0;
|
1312 |
-
for ($i=0; $i<$length; $i++) {
|
1313 |
-
|
1314 |
-
// Get info about the next character
|
1315 |
-
$char_length = ($content[$i] == '=') ? 3 : 1;
|
1316 |
-
$char = $content[$i];
|
1317 |
-
if ($char_length == 3) {
|
1318 |
-
$char .= $content[$i+1] . $content[$i+2];
|
1319 |
-
}
|
1320 |
-
|
1321 |
-
// Skip characters if we have an encoded character, this must be
|
1322 |
-
// done before checking for whitespace at the beginning and end of
|
1323 |
-
// lines or else characters in the content will be skipped
|
1324 |
-
$i += $char_length-1;
|
1325 |
-
|
1326 |
-
// Spaces and tabs at the beginning and ending of lines have to be encoded
|
1327 |
-
$begining_or_end = $line_length > 69 || $line_length == 0;
|
1328 |
-
$tab_or_space = $char == ' ' || $char == "\t";
|
1329 |
-
if ($begining_or_end && $tab_or_space) {
|
1330 |
-
$char_length = 3;
|
1331 |
-
$char = ($char == ' ') ? '=20' : '=09';
|
1332 |
-
}
|
1333 |
-
|
1334 |
-
// If we have too long a line, wrap it
|
1335 |
-
if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
|
1336 |
-
$output .= "=\r\n";
|
1337 |
-
$line_length = 0;
|
1338 |
-
}
|
1339 |
-
|
1340 |
-
// Add the character
|
1341 |
-
$output .= $char;
|
1342 |
-
|
1343 |
-
// Figure out how much longer the line is now
|
1344 |
-
if ($char == "\r" || $char == "\n") {
|
1345 |
-
$line_length = 0;
|
1346 |
-
} else {
|
1347 |
-
$line_length += $char_length;
|
1348 |
-
}
|
1349 |
-
}
|
1350 |
-
|
1351 |
-
return $output;
|
1352 |
-
}
|
1353 |
-
|
1354 |
-
|
1355 |
-
/**
|
1356 |
-
* Sends the email
|
1357 |
-
*
|
1358 |
-
* The return value is the message id, which should be included as the
|
1359 |
-
* `Message-ID` header of the email. While almost all SMTP servers will not
|
1360 |
-
* modify this value, testing has indicated at least one (smtp.live.com
|
1361 |
-
* for Windows Live Mail) does.
|
1362 |
-
*
|
1363 |
-
* @throws fValidationException When ::validate() throws an exception
|
1364 |
-
*
|
1365 |
-
* @param fSMTP $connection The SMTP connection to send the message over
|
1366 |
-
* @return string The message id for the message - see method description for details
|
1367 |
-
*/
|
1368 |
-
public function send($connection=NULL)
|
1369 |
-
{
|
1370 |
-
$this->validate();
|
1371 |
-
|
1372 |
-
// The mail() function on Windows doesn't support names in headers so
|
1373 |
-
// we must strip them down to just the email address
|
1374 |
-
if ($connection === NULL && fCore::checkOS('windows')) {
|
1375 |
-
$vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
|
1376 |
-
foreach ($vars as $var) {
|
1377 |
-
if (!is_array($this->$var)) {
|
1378 |
-
if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
|
1379 |
-
$this->$var = $match[2];
|
1380 |
-
}
|
1381 |
-
} else {
|
1382 |
-
$new_emails = array();
|
1383 |
-
foreach ($this->$var as $email) {
|
1384 |
-
if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
|
1385 |
-
$email = $match[2];
|
1386 |
-
}
|
1387 |
-
$new_emails[] = $email;
|
1388 |
-
}
|
1389 |
-
$this->$var = $new_emails;
|
1390 |
-
}
|
1391 |
-
}
|
1392 |
-
}
|
1393 |
-
|
1394 |
-
$to = substr(trim($this->buildMultiAddressHeader("To", $this->to_emails)), 4);
|
1395 |
-
|
1396 |
-
$top_level_boundary = $this->createBoundary();
|
1397 |
-
$headers = $this->createHeaders($top_level_boundary, $this->message_id);
|
1398 |
-
|
1399 |
-
$subject = str_replace(array("\r", "\n"), '', $this->subject);
|
1400 |
-
$subject = self::makeEncodedWord($subject, 9);
|
1401 |
-
|
1402 |
-
$body = $this->createBody($top_level_boundary);
|
1403 |
-
|
1404 |
-
if ($this->smime_encrypt || $this->smime_sign) {
|
1405 |
-
list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
|
1406 |
-
}
|
1407 |
-
|
1408 |
-
// Remove extra line breaks
|
1409 |
-
$headers = trim($headers);
|
1410 |
-
$body = trim($body);
|
1411 |
-
|
1412 |
-
if ($connection) {
|
1413 |
-
$to_emails = $this->extractEmails($this->to_emails);
|
1414 |
-
$to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
|
1415 |
-
$to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
|
1416 |
-
$from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
|
1417 |
-
$connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
|
1418 |
-
return $this->message_id;
|
1419 |
-
}
|
1420 |
-
|
1421 |
-
// Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
|
1422 |
-
$parameters = NULL;
|
1423 |
-
if (!fCore::checkOS('windows') && $this->bounce_to_email) {
|
1424 |
-
preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
|
1425 |
-
$parameters = '-f ' . $matches[0];
|
1426 |
-
|
1427 |
-
// Windows takes the Return-Path email from the sendmail_from ini setting
|
1428 |
-
} elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
|
1429 |
-
$old_sendmail_from = ini_get('sendmail_from');
|
1430 |
-
preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
|
1431 |
-
ini_set('sendmail_from', $matches[0]);
|
1432 |
-
}
|
1433 |
-
|
1434 |
-
// This is a gross qmail fix that is a last resort
|
1435 |
-
if (self::$popen_sendmail || self::$convert_crlf) {
|
1436 |
-
$to = str_replace("\r\n", "\n", $to);
|
1437 |
-
$subject = str_replace("\r\n", "\n", $subject);
|
1438 |
-
$body = str_replace("\r\n", "\n", $body);
|
1439 |
-
$headers = str_replace("\r\n", "\n", $headers);
|
1440 |
-
}
|
1441 |
-
|
1442 |
-
// If the user is using qmail and wants to try to fix the \r\r\n line break issue
|
1443 |
-
if (self::$popen_sendmail) {
|
1444 |
-
$sendmail_command = ini_get('sendmail_path');
|
1445 |
-
if ($parameters) {
|
1446 |
-
$sendmail_command .= ' ' . $parameters;
|
1447 |
-
}
|
1448 |
-
|
1449 |
-
$sendmail_process = popen($sendmail_command, 'w');
|
1450 |
-
fprintf($sendmail_process, "To: %s\n", $to);
|
1451 |
-
fprintf($sendmail_process, "Subject: %s\n", $subject);
|
1452 |
-
if ($headers) {
|
1453 |
-
fprintf($sendmail_process, "%s\n", $headers);
|
1454 |
-
}
|
1455 |
-
fprintf($sendmail_process, "\n%s\n", $body);
|
1456 |
-
$error = pclose($sendmail_process);
|
1457 |
-
|
1458 |
-
// This is the normal way to send mail
|
1459 |
-
} else {
|
1460 |
-
// On Windows, mail() sends directly to an SMTP server and will
|
1461 |
-
// strip a leading . from the body
|
1462 |
-
if (fCore::checkOS('windows')) {
|
1463 |
-
$body = preg_replace('#^\.#', '..', $body);
|
1464 |
-
}
|
1465 |
-
|
1466 |
-
if ($parameters) {
|
1467 |
-
$error = !mail($to, $subject, $body, $headers, $parameters);
|
1468 |
-
} else {
|
1469 |
-
$error = !mail($to, $subject, $body, $headers);
|
1470 |
-
}
|
1471 |
-
}
|
1472 |
-
|
1473 |
-
if (fCore::checkOS('windows') && $this->bounce_to_email) {
|
1474 |
-
ini_set('sendmail_from', $old_sendmail_from);
|
1475 |
-
}
|
1476 |
-
|
1477 |
-
if ($error) {
|
1478 |
-
throw new fConnectivityException(
|
1479 |
-
'An error occured while trying to send the email entitled %s',
|
1480 |
-
$this->subject
|
1481 |
-
);
|
1482 |
-
}
|
1483 |
-
|
1484 |
-
return $this->message_id;
|
1485 |
-
}
|
1486 |
-
|
1487 |
-
|
1488 |
-
/**
|
1489 |
-
* Sets the plaintext version of the email body
|
1490 |
-
*
|
1491 |
-
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1492 |
-
* http://flourishlib.com/docs/UTF-8 for more information.
|
1493 |
-
*
|
1494 |
-
* @param string $plaintext The plaintext version of the email body
|
1495 |
-
* @param boolean $unindent_expand_constants If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
|
1496 |
-
* @return fEmail The email object, to allow for method chaining
|
1497 |
-
*/
|
1498 |
-
public function setBody($plaintext, $unindent_expand_constants=FALSE)
|
1499 |
-
{
|
1500 |
-
if ($unindent_expand_constants) {
|
1501 |
-
$plaintext = self::unindentExpand($plaintext);
|
1502 |
-
}
|
1503 |
-
|
1504 |
-
$this->plaintext_body = $plaintext;
|
1505 |
-
|
1506 |
-
return $this;
|
1507 |
-
}
|
1508 |
-
|
1509 |
-
|
1510 |
-
/**
|
1511 |
-
* Adds the email address the email will be bounced to
|
1512 |
-
*
|
1513 |
-
* This email address will be set to the `Return-Path` header.
|
1514 |
-
*
|
1515 |
-
* @param string $email The email address to bounce to
|
1516 |
-
* @return fEmail The email object, to allow for method chaining
|
1517 |
-
*/
|
1518 |
-
public function setBounceToEmail($email)
|
1519 |
-
{
|
1520 |
-
if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
|
1521 |
-
throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
|
1522 |
-
}
|
1523 |
-
if (!$email) {
|
1524 |
-
return;
|
1525 |
-
}
|
1526 |
-
|
1527 |
-
$this->bounce_to_email = self::combineNameEmail('', $email);
|
1528 |
-
|
1529 |
-
return $this;
|
1530 |
-
}
|
1531 |
-
|
1532 |
-
|
1533 |
-
/**
|
1534 |
-
* Adds the `From:` email address to the email
|
1535 |
-
*
|
1536 |
-
* @param string $email The email address being sent from
|
1537 |
-
* @param string $name The from email user's name - unfortunately on windows this is ignored
|
1538 |
-
* @return fEmail The email object, to allow for method chaining
|
1539 |
-
*/
|
1540 |
-
public function setFromEmail($email, $name=NULL)
|
1541 |
-
{
|
1542 |
-
if (!$email) {
|
1543 |
-
return;
|
1544 |
-
}
|
1545 |
-
|
1546 |
-
$this->from_email = self::combineNameEmail($name, $email);
|
1547 |
-
|
1548 |
-
return $this;
|
1549 |
-
}
|
1550 |
-
|
1551 |
-
|
1552 |
-
/**
|
1553 |
-
* Sets the HTML version of the email body
|
1554 |
-
*
|
1555 |
-
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1556 |
-
* http://flourishlib.com/docs/UTF-8 for more information.
|
1557 |
-
*
|
1558 |
-
* @param string $html The HTML version of the email body
|
1559 |
-
* @return fEmail The email object, to allow for method chaining
|
1560 |
-
*/
|
1561 |
-
public function setHTMLBody($html)
|
1562 |
-
{
|
1563 |
-
$this->html_body = $html;
|
1564 |
-
|
1565 |
-
return $this;
|
1566 |
-
}
|
1567 |
-
|
1568 |
-
|
1569 |
-
/**
|
1570 |
-
* Adds the `Reply-To:` email address to the email
|
1571 |
-
*
|
1572 |
-
* @param string $email The email address to reply to
|
1573 |
-
* @param string $name The reply-to email user's name
|
1574 |
-
* @return fEmail The email object, to allow for method chaining
|
1575 |
-
*/
|
1576 |
-
public function setReplyToEmail($email, $name=NULL)
|
1577 |
-
{
|
1578 |
-
if (!$email) {
|
1579 |
-
return;
|
1580 |
-
}
|
1581 |
-
|
1582 |
-
$this->reply_to_email = self::combineNameEmail($name, $email);
|
1583 |
-
|
1584 |
-
return $this;
|
1585 |
-
}
|
1586 |
-
|
1587 |
-
|
1588 |
-
/**
|
1589 |
-
* Adds the `Sender:` email address to the email
|
1590 |
-
*
|
1591 |
-
* The `Sender:` header is used to indicate someone other than the `From:`
|
1592 |
-
* address is actually submitting the message to the network.
|
1593 |
-
*
|
1594 |
-
* @param string $email The email address the message is actually being sent from
|
1595 |
-
* @param string $name The sender email user's name
|
1596 |
-
* @return fEmail The email object, to allow for method chaining
|
1597 |
-
*/
|
1598 |
-
public function setSenderEmail($email, $name=NULL)
|
1599 |
-
{
|
1600 |
-
if (!$email) {
|
1601 |
-
return;
|
1602 |
-
}
|
1603 |
-
|
1604 |
-
$this->sender_email = self::combineNameEmail($name, $email);
|
1605 |
-
|
1606 |
-
return $this;
|
1607 |
-
}
|
1608 |
-
|
1609 |
-
|
1610 |
-
/**
|
1611 |
-
* Sets the subject of the email
|
1612 |
-
*
|
1613 |
-
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1614 |
-
* http://flourishlib.com/docs/UTF-8 for more information.
|
1615 |
-
*
|
1616 |
-
* @param string $subject The subject of the email
|
1617 |
-
* @return fEmail The email object, to allow for method chaining
|
1618 |
-
*/
|
1619 |
-
public function setSubject($subject)
|
1620 |
-
{
|
1621 |
-
$this->subject = $subject;
|
1622 |
-
|
1623 |
-
return $this;
|
1624 |
-
}
|
1625 |
-
|
1626 |
-
|
1627 |
-
/**
|
1628 |
-
* Sets the email to be signed with S/MIME
|
1629 |
-
*
|
1630 |
-
* @param string $senders_smime_cert_file The file path to the sender's PEM-encoded S/MIME certificate
|
1631 |
-
* @param string $senders_smime_pk_file The file path to the sender's S/MIME private key
|
1632 |
-
* @param string $senders_smime_pk_password The password for the sender's S/MIME private key
|
1633 |
-
* @return fEmail The email object, to allow for method chaining
|
1634 |
-
*/
|
1635 |
-
public function sign($senders_smime_cert_file, $senders_smime_pk_file, $senders_smime_pk_password)
|
1636 |
-
{
|
1637 |
-
if (!extension_loaded('openssl')) {
|
1638 |
-
throw new fEnvironmentException(
|
1639 |
-
'An S/MIME signature was requested for an email, but the %s extension is not installed',
|
1640 |
-
'openssl'
|
1641 |
-
);
|
1642 |
-
}
|
1643 |
-
|
1644 |
-
if (!self::stringlike($senders_smime_cert_file)) {
|
1645 |
-
throw new fProgrammerException(
|
1646 |
-
"The sender's S/MIME certificate file specified, %s, does not appear to be a valid filename",
|
1647 |
-
$senders_smime_cert_file
|
1648 |
-
);
|
1649 |
-
}
|
1650 |
-
if (!file_exists($senders_smime_cert_file) || !is_readable($senders_smime_cert_file)) {
|
1651 |
-
throw new fEnvironmentException(
|
1652 |
-
"The sender's S/MIME certificate file specified, %s, does not exist or could not be read",
|
1653 |
-
$senders_smime_cert_file
|
1654 |
-
);
|
1655 |
-
}
|
1656 |
-
|
1657 |
-
if (!self::stringlike($senders_smime_pk_file)) {
|
1658 |
-
throw new fProgrammerException(
|
1659 |
-
"The sender's S/MIME primary key file specified, %s, does not appear to be a valid filename",
|
1660 |
-
$senders_smime_pk_file
|
1661 |
-
);
|
1662 |
-
}
|
1663 |
-
if (!file_exists($senders_smime_pk_file) || !is_readable($senders_smime_pk_file)) {
|
1664 |
-
throw new fEnvironmentException(
|
1665 |
-
"The sender's S/MIME primary key file specified, %s, does not exist or could not be read",
|
1666 |
-
$senders_smime_pk_file
|
1667 |
-
);
|
1668 |
-
}
|
1669 |
-
|
1670 |
-
$this->smime_sign = TRUE;
|
1671 |
-
$this->senders_smime_cert_file = $senders_smime_cert_file;
|
1672 |
-
$this->senders_smime_pk_file = $senders_smime_pk_file;
|
1673 |
-
$this->senders_smime_pk_password = $senders_smime_pk_password;
|
1674 |
-
|
1675 |
-
return $this;
|
1676 |
-
}
|
1677 |
-
|
1678 |
-
|
1679 |
-
/**
|
1680 |
-
* Validates that all of the parts of the email are valid
|
1681 |
-
*
|
1682 |
-
* @throws fValidationException When part of the email is missing or formatted incorrectly
|
1683 |
-
*
|
1684 |
-
* @return void
|
1685 |
-
*/
|
1686 |
-
private function validate()
|
1687 |
-
{
|
1688 |
-
$validation_messages = array();
|
1689 |
-
|
1690 |
-
// Check all multi-address email field
|
1691 |
-
$multi_address_field_list = array(
|
1692 |
-
'to_emails' => self::compose('recipient'),
|
1693 |
-
'cc_emails' => self::compose('CC recipient'),
|
1694 |
-
'bcc_emails' => self::compose('BCC recipient')
|
1695 |
-
);
|
1696 |
-
|
1697 |
-
foreach ($multi_address_field_list as $field => $name) {
|
1698 |
-
foreach ($this->$field as $email) {
|
1699 |
-
if ($email && !preg_match(self::NAME_EMAIL_REGEX, $email) && !preg_match(self::EMAIL_REGEX, $email)) {
|
1700 |
-
$validation_messages[] = htmlspecialchars(self::compose(
|
1701 |
-
'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
|
1702 |
-
$name,
|
1703 |
-
$email
|
1704 |
-
), ENT_QUOTES, 'UTF-8');
|
1705 |
-
}
|
1706 |
-
}
|
1707 |
-
}
|
1708 |
-
|
1709 |
-
// Check all single-address email fields
|
1710 |
-
$single_address_field_list = array(
|
1711 |
-
'from_email' => self::compose('From email address'),
|
1712 |
-
'reply_to_email' => self::compose('Reply-To email address'),
|
1713 |
-
'sender_email' => self::compose('Sender email address'),
|
1714 |
-
'bounce_to_email' => self::compose('Bounce-To email address')
|
1715 |
-
);
|
1716 |
-
|
1717 |
-
foreach ($single_address_field_list as $field => $name) {
|
1718 |
-
if ($this->$field && !preg_match(self::NAME_EMAIL_REGEX, $this->$field) && !preg_match(self::EMAIL_REGEX, $this->$field)) {
|
1719 |
-
$validation_messages[] = htmlspecialchars(self::compose(
|
1720 |
-
'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
|
1721 |
-
$name,
|
1722 |
-
$this->$field
|
1723 |
-
), ENT_QUOTES, 'UTF-8');
|
1724 |
-
}
|
1725 |
-
}
|
1726 |
-
|
1727 |
-
// Make sure the required fields are all set
|
1728 |
-
if (!$this->to_emails) {
|
1729 |
-
$validation_messages[] = self::compose(
|
1730 |
-
"Please provide at least one recipient"
|
1731 |
-
);
|
1732 |
-
}
|
1733 |
-
|
1734 |
-
if (!$this->from_email) {
|
1735 |
-
$validation_messages[] = self::compose(
|
1736 |
-
"Please provide the from email address"
|
1737 |
-
);
|
1738 |
-
}
|
1739 |
-
|
1740 |
-
if (!self::stringlike($this->subject)) {
|
1741 |
-
$validation_messages[] = self::compose(
|
1742 |
-
"Please provide an email subject"
|
1743 |
-
);
|
1744 |
-
}
|
1745 |
-
|
1746 |
-
if (strpos($this->subject, "\n") !== FALSE) {
|
1747 |
-
$validation_messages[] = self::compose(
|
1748 |
-
"The subject contains one or more newline characters"
|
1749 |
-
);
|
1750 |
-
}
|
1751 |
-
|
1752 |
-
if (!self::stringlike($this->plaintext_body)) {
|
1753 |
-
$validation_messages[] = self::compose(
|
1754 |
-
"Please provide a plaintext email body"
|
1755 |
-
);
|
1756 |
-
}
|
1757 |
-
|
1758 |
-
// Make sure the attachments look good
|
1759 |
-
foreach ($this->attachments as $filename => $file_info) {
|
1760 |
-
if (!self::stringlike($file_info['mime-type'])) {
|
1761 |
-
$validation_messages[] = self::compose(
|
1762 |
-
"No mime-type was specified for the attachment %s",
|
1763 |
-
$filename
|
1764 |
-
);
|
1765 |
-
}
|
1766 |
-
if (!self::stringlike($file_info['contents'])) {
|
1767 |
-
$validation_messages[] = self::compose(
|
1768 |
-
"The attachment %s appears to be a blank file",
|
1769 |
-
$filename
|
1770 |
-
);
|
1771 |
-
}
|
1772 |
-
}
|
1773 |
-
|
1774 |
-
if ($validation_messages) {
|
1775 |
-
throw new fValidationException(
|
1776 |
-
'The email could not be sent because:',
|
1777 |
-
$validation_messages
|
1778 |
-
);
|
1779 |
-
}
|
1780 |
-
}
|
1781 |
}
|
1782 |
|
1783 |
-
|
1784 |
-
|
1785 |
/**
|
1786 |
* Copyright (c) 2008-2011 Will Bond <will@flourishlib.com>, others
|
1787 |
*
|
1 |
<?php
|
2 |
+
|
3 |
/**
|
4 |
* Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
|
5 |
*
|
51 |
* @changes 1.0.0b2 Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
|
52 |
* @changes 1.0.0b The initial implementation [wb, 2008-06-23]
|
53 |
*/
|
54 |
+
class fEmail {
|
55 |
+
|
56 |
+
// The following constants allow for nice looking callbacks to static methods
|
57 |
+
const combineNameEmail = 'fEmail::combineNameEmail';
|
58 |
+
const fixQmail = 'fEmail::fixQmail';
|
59 |
+
const getFQDN = 'fEmail::getFQDN';
|
60 |
+
const reset = 'fEmail::reset';
|
61 |
+
const unindentExpand = 'fEmail::unindentExpand';
|
62 |
+
|
63 |
+
/**
|
64 |
+
* A regular expression to match an email address, exluding those with comments and folding whitespace
|
65 |
+
*
|
66 |
+
* The matches will be:
|
67 |
+
*
|
68 |
+
* - `[0]`: The whole email address
|
69 |
+
* - `[1]`: The name before the `@`
|
70 |
+
* - `[2]`: The domain/ip after the `@`
|
71 |
+
*
|
72 |
+
* @var string
|
73 |
+
*/
|
74 |
+
const EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
|
|
|
75 |
(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
|
76 |
(?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
|
77 |
)@( # The @ symbol
|
79 |
\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
|
80 |
)[ \t]*$~ixD'; # Allow Trailing whitespace
|
81 |
|
82 |
+
/**
|
83 |
+
* A regular expression to match a `name <email>` string, exluding those with comments and folding whitespace
|
84 |
+
*
|
85 |
+
* The matches will be:
|
86 |
+
*
|
87 |
+
* - `[0]`: The whole name and email address
|
88 |
+
* - `[1]`: The name
|
89 |
+
* - `[2]`: The whole email address
|
90 |
+
* - `[3]`: The email username before the `@`
|
91 |
+
* - `[4]`: The email domain/ip after the `@`
|
92 |
+
*
|
93 |
+
* @var string
|
94 |
+
*/
|
95 |
+
const NAME_EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
|
96 |
(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*) # An "atom" or a quoted string
|
97 |
(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*) # Another "atom" or a quoted string or a . followed by one of those, any number of times
|
98 |
[ \t]*<[ \t]*(( # The < encapsulating the email address
|
103 |
\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
|
104 |
))[ \t]*>[ \t]*$~ixD'; # Closing > and trailing whitespace
|
105 |
|
106 |
+
/**
|
107 |
+
* Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
|
108 |
+
*
|
109 |
+
* @var boolean
|
110 |
+
*/
|
111 |
+
|
112 |
+
static private $convert_crlf = FALSE;
|
113 |
+
|
114 |
+
/**
|
115 |
+
* The local fully-qualified domain name
|
116 |
+
*/
|
117 |
+
static private $fqdn;
|
118 |
+
|
119 |
+
/**
|
120 |
+
* Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
|
121 |
+
*
|
122 |
+
* @var boolean
|
123 |
+
*/
|
124 |
+
static private $popen_sendmail = FALSE;
|
125 |
+
|
126 |
+
/**
|
127 |
+
* Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
|
128 |
+
*
|
129 |
+
* This method will remove newline characters from the name and email, and
|
130 |
+
* will remove any backslash (`\`) and double quote (`"`) characters from
|
131 |
+
* the name.
|
132 |
+
*
|
133 |
+
* @internal
|
134 |
+
*
|
135 |
+
* @param string $name The name associated with the email address
|
136 |
+
* @param string $email The email address
|
137 |
+
* @return string The '"name" <email>' or 'email' string
|
138 |
+
*/
|
139 |
+
static public function combineNameEmail($name, $email) {
|
140 |
+
// Strip lower ascii character since they aren't useful in email addresses
|
141 |
+
$email = preg_replace('#[\x0-\x19]+#', '', $email);
|
142 |
+
$name = preg_replace('#[\x0-\x19]+#', '', $name);
|
143 |
+
|
144 |
+
if (!$name) {
|
145 |
+
return $email;
|
146 |
+
}
|
147 |
+
|
148 |
+
// If the name contains any non-ascii bytes or stuff not allowed
|
149 |
+
// in quoted strings we just make an encoded word out of it
|
150 |
+
if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
|
151 |
+
// The longest header name that will contain email addresses is
|
152 |
+
// "Bcc: ", which is 5 characters long
|
153 |
+
$name = self::makeEncodedWord($name, 5);
|
154 |
+
} else {
|
155 |
+
$name = '"' . $name . '"';
|
156 |
+
}
|
157 |
+
|
158 |
+
return $name . ' <' . $email . '>';
|
159 |
+
}
|
160 |
+
|
161 |
+
/**
|
162 |
+
* Composes text using fText if loaded
|
163 |
+
*
|
164 |
+
* @param string $message The message to compose
|
165 |
+
* @param mixed $component A string or number to insert into the message
|
166 |
+
* @param mixed ...
|
167 |
+
* @return string The composed and possible translated message
|
168 |
+
*/
|
169 |
+
static protected function compose($message) {
|
170 |
+
$args = array_slice(func_get_args(), 1);
|
171 |
+
|
172 |
+
if (class_exists('fText', FALSE)) {
|
173 |
+
return call_user_func_array(
|
174 |
+
array('fText', 'compose'), array($message, $args)
|
175 |
+
);
|
176 |
+
} else {
|
177 |
+
return vsprintf($message, $args);
|
178 |
+
}
|
179 |
+
}
|
180 |
+
|
181 |
+
/**
|
182 |
+
* Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
|
183 |
+
*
|
184 |
+
* Before trying to fix qmail with this method, please try using fSMTP
|
185 |
+
* to connect to `localhost` and pass the fSMTP object to ::send().
|
186 |
+
*
|
187 |
+
* @return void
|
188 |
+
*/
|
189 |
+
static public function fixQmail() {
|
190 |
+
if (fCore::checkOS('windows')) {
|
191 |
+
return;
|
192 |
+
}
|
193 |
+
|
194 |
+
$sendmail_command = ini_get('sendmail_path');
|
195 |
+
|
196 |
+
if (!$sendmail_command) {
|
197 |
+
self::$convert_crlf = TRUE;
|
198 |
+
trigger_error(
|
199 |
+
self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'), E_USER_WARNING
|
200 |
+
);
|
201 |
+
trigger_error(
|
202 |
+
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'), E_USER_WARNING
|
203 |
+
);
|
204 |
+
}
|
205 |
+
|
206 |
+
$sendmail_command_parts = explode(' ', $sendmail_command, 2);
|
207 |
+
|
208 |
+
$sendmail_path = $sendmail_command_parts[0];
|
209 |
+
$sendmail_dir = pathinfo($sendmail_path, PATHINFO_DIRNAME);
|
210 |
+
$sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
|
211 |
+
|
212 |
+
// Check to see if we can run sendmail via popen
|
213 |
+
$executable = FALSE;
|
214 |
+
$safe_mode = FALSE;
|
215 |
+
|
216 |
+
if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
|
217 |
+
$safe_mode = TRUE;
|
218 |
+
$exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
|
219 |
+
foreach ($exec_dirs as $exec_dir) {
|
220 |
+
if (stripos($sendmail_dir, $exec_dir) !== 0) {
|
221 |
+
continue;
|
222 |
+
}
|
223 |
+
if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
|
224 |
+
$executable = TRUE;
|
225 |
+
}
|
226 |
+
}
|
227 |
+
} else {
|
228 |
+
if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
|
229 |
+
$executable = TRUE;
|
230 |
+
}
|
231 |
+
}
|
232 |
+
|
233 |
+
if ($executable) {
|
234 |
+
self::$popen_sendmail = TRUE;
|
235 |
+
} else {
|
236 |
+
self::$convert_crlf = TRUE;
|
237 |
+
if ($safe_mode) {
|
238 |
+
trigger_error(
|
239 |
+
self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'), E_USER_WARNING
|
240 |
+
);
|
241 |
+
trigger_error(
|
242 |
+
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'), E_USER_WARNING
|
243 |
+
);
|
244 |
+
} else {
|
245 |
+
trigger_error(
|
246 |
+
self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'), E_USER_WARNING
|
247 |
+
);
|
248 |
+
trigger_error(
|
249 |
+
self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'), E_USER_WARNING
|
250 |
+
);
|
251 |
+
}
|
252 |
+
}
|
253 |
+
}
|
254 |
+
|
255 |
+
/**
|
256 |
+
* Returns the fully-qualified domain name of the server
|
257 |
+
*
|
258 |
+
* @internal
|
259 |
+
*
|
260 |
+
* @return string The fully-qualified domain name of the server
|
261 |
+
*/
|
262 |
+
static public function getFQDN() {
|
263 |
+
if (self::$fqdn !== NULL) {
|
264 |
+
return self::$fqdn;
|
265 |
+
}
|
266 |
+
|
267 |
+
if (isset($_ENV['HOST'])) {
|
268 |
+
self::$fqdn = $_ENV['HOST'];
|
269 |
+
}
|
270 |
+
if (strpos(self::$fqdn, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
|
271 |
+
self::$fqdn = $_ENV['HOSTNAME'];
|
272 |
+
}
|
273 |
+
if (strpos(self::$fqdn, '.') === FALSE) {
|
274 |
+
self::$fqdn = php_uname('n');
|
275 |
+
}
|
276 |
+
|
277 |
+
if (strpos(self::$fqdn, '.') === FALSE) {
|
278 |
+
|
279 |
+
$can_exec = !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode');
|
280 |
+
if (fCore::checkOS('linux') && $can_exec) {
|
281 |
+
self::$fqdn = trim(shell_exec('hostname --fqdn'));
|
282 |
+
} elseif (fCore::checkOS('windows')) {
|
283 |
+
$shell = new COM('WScript.Shell');
|
284 |
+
$tcpip_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip';
|
285 |
+
try {
|
286 |
+
$domain = $shell->RegRead($tcpip_key . '\Parameters\NV Domain');
|
287 |
+
} catch (com_exception $e) {
|
288 |
+
try {
|
289 |
+
$domain = $shell->RegRead($tcpip_key . '\Parameters\DhcpDomain');
|
290 |
+
} catch (com_exception $e) {
|
291 |
+
try {
|
292 |
+
$adapters = $shell->RegRead($tcpip_key . '\Linkage\Route');
|
293 |
+
foreach ($adapters as $adapter) {
|
294 |
+
if ($adapter[0] != '{') {
|
295 |
+
continue;
|
296 |
+
}
|
297 |
+
try {
|
298 |
+
$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\Domain');
|
299 |
+
} catch (com_exception $e) {
|
300 |
+
try {
|
301 |
+
$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\DhcpDomain');
|
302 |
+
} catch (com_exception $e) {
|
303 |
+
|
304 |
+
}
|
305 |
+
}
|
306 |
+
}
|
307 |
+
} catch (com_exception $e) {
|
308 |
+
|
309 |
+
}
|
310 |
+
}
|
311 |
+
}
|
312 |
+
if (!empty($domain)) {
|
313 |
+
self::$fqdn .= '.' . $domain;
|
314 |
+
}
|
315 |
+
} elseif (!fCore::checkOS('windows') && !ini_get('open_basedir') && file_exists('/etc/resolv.conf')) {
|
316 |
+
$output = file_get_contents('/etc/resolv.conf');
|
317 |
+
if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
|
318 |
+
self::$fqdn .= '.' . $match[1];
|
319 |
+
}
|
320 |
+
}
|
321 |
+
}
|
322 |
+
|
323 |
+
return self::$fqdn;
|
324 |
+
}
|
325 |
+
|
326 |
+
/**
|
327 |
+
* Encodes a string to UTF-8 encoded-word
|
328 |
+
*
|
329 |
+
* @param string $content The content to encode
|
330 |
+
* @param integer $first_line_prefix_length The length of any prefix applied to the first line of the encoded word - this allows properly accounting for a header name
|
331 |
+
* @return string The encoded string
|
332 |
+
*/
|
333 |
+
static private function makeEncodedWord($content, $first_line_prefix_length) {
|
334 |
+
// Homogenize the line-endings to CRLF
|
335 |
+
$content = str_replace("\r\n", "\n", $content);
|
336 |
+
$content = str_replace("\r", "\n", $content);
|
337 |
+
$content = str_replace("\n", "\r\n", $content);
|
338 |
+
|
339 |
+
// Encoded word is not required if all characters are ascii
|
340 |
+
if (!preg_match('#[\x80-\xFF]#', $content)) {
|
341 |
+
return $content;
|
342 |
+
}
|
343 |
+
|
344 |
+
// A quick a dirty hex encoding
|
345 |
+
$content = rawurlencode($content);
|
346 |
+
$content = str_replace('=', '%3D', $content);
|
347 |
+
$content = str_replace('%', '=', $content);
|
348 |
+
|
349 |
+
// Decode characters that don't have to be coded
|
350 |
+
$decodings = array(
|
351 |
+
'=20' => '_', '=21' => '!', '=22' => '"', '=23' => '#',
|
352 |
+
'=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
|
353 |
+
'=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
|
354 |
+
'=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
|
355 |
+
'=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
|
356 |
+
'=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
|
357 |
+
'=5E' => '^', '=60' => '`', '=7B' => '{', '=7C' => '|',
|
358 |
+
'=7D' => '}', '=7E' => '~', ' ' => '_'
|
359 |
+
);
|
360 |
+
|
361 |
+
$content = strtr($content, $decodings);
|
362 |
+
|
363 |
+
$length = strlen($content);
|
364 |
+
|
365 |
+
$prefix = '=?utf-8?Q?';
|
366 |
+
$suffix = '?=';
|
367 |
+
|
368 |
+
$prefix_length = 10;
|
369 |
+
$suffix_length = 2;
|
370 |
+
|
371 |
+
// This loop goes through and ensures we are wrapping by 75 chars
|
372 |
+
// including the encoded word delimiters
|
373 |
+
$output = $prefix;
|
374 |
+
$line_length = $prefix_length + $first_line_prefix_length;
|
375 |
+
|
376 |
+
for ($i = 0; $i < $length; $i++) {
|
377 |
+
|
378 |
+
// Get info about the next character
|
379 |
+
$char_length = ($content[$i] == '=') ? 3 : 1;
|
380 |
+
$char = $content[$i];
|
381 |
+
if ($char_length == 3) {
|
382 |
+
$char .= $content[$i + 1] . $content[$i + 2];
|
383 |
+
}
|
384 |
+
|
385 |
+
// If we have too long a line, wrap it
|
386 |
+
if ($line_length + $suffix_length + $char_length > 75) {
|
387 |
+
$output .= $suffix . "\r\n " . $prefix;
|
388 |
+
$line_length = $prefix_length + 2;
|
389 |
+
}
|
390 |
+
|
391 |
+
// Add the character
|
392 |
+
$output .= $char;
|
393 |
+
|
394 |
+
// Figure out how much longer the line is
|
395 |
+
$line_length += $char_length;
|
396 |
+
|
397 |
+
// Skip characters if we have an encoded character
|
398 |
+
$i += $char_length - 1;
|
399 |
+
}
|
400 |
+
|
401 |
+
if (substr($output, -2) != $suffix) {
|
402 |
+
$output .= $suffix;
|
403 |
+
}
|
404 |
+
|
405 |
+
return $output;
|
406 |
+
}
|
407 |
+
|
408 |
+
/**
|
409 |
+
* Resets the configuration of the class
|
410 |
+
*
|
411 |
+
* @internal
|
412 |
+
*
|
413 |
+
* @return void
|
414 |
+
*/
|
415 |
+
static public function reset() {
|
416 |
+
self::$convert_crlf = FALSE;
|
417 |
+
self::$fqdn = NULL;
|
418 |
+
self::$popen_sendmail = FALSE;
|
419 |
+
}
|
420 |
+
|
421 |
+
/**
|
422 |
+
* Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
|
423 |
+
*
|
424 |
+
* @param mixed $value The value to check
|
425 |
+
* @return boolean If the value is string-like
|
426 |
+
*/
|
427 |
+
static protected function stringlike($value) {
|
428 |
+
if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
|
429 |
+
return FALSE;
|
430 |
+
}
|
431 |
+
|
432 |
+
return TRUE;
|
433 |
+
}
|
434 |
+
|
435 |
+
/**
|
436 |
+
* Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
|
437 |
+
*
|
438 |
+
* @param string $text The text to unindent and replace constants in
|
439 |
+
* @return string The unindented text
|
440 |
+
*/
|
441 |
+
static public function unindentExpand($text) {
|
442 |
+
$text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
|
443 |
+
|
444 |
+
if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
|
445 |
+
$text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
|
446 |
+
}
|
447 |
+
|
448 |
+
preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
|
449 |
+
foreach ($constants as $constant) {
|
450 |
+
if (!defined($constant[1])) {
|
451 |
+
continue;
|
452 |
+
}
|
453 |
+
$text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
|
454 |
+
}
|
455 |
+
|
456 |
+
return $text;
|
457 |
+
}
|
458 |
+
|
459 |
+
/**
|
460 |
+
* The file contents to attach
|
461 |
+
*
|
462 |
+
* @var array
|
463 |
+
*/
|
464 |
+
private $attachments = array();
|
465 |
+
|
466 |
+
/**
|
467 |
+
* The email address(es) to BCC to
|
468 |
+
*
|
469 |
+
* @var array
|
470 |
+
*/
|
471 |
+
private $bcc_emails = array();
|
472 |
+
|
473 |
+
/**
|
474 |
+
* The email address to bounce to
|
475 |
+
*
|
476 |
+
* @var string
|
477 |
+
*/
|
478 |
+
private $bounce_to_email = NULL;
|
479 |
+
|
480 |
+
/**
|
481 |
+
* The email address(es) to CC to
|
482 |
+
*
|
483 |
+
* @var array
|
484 |
+
*/
|
485 |
+
private $cc_emails = array();
|
486 |
+
|
487 |
+
/**
|
488 |
+
* Custom headers
|
489 |
+
*
|
490 |
+
* @var array
|
491 |
+
*/
|
492 |
+
private $custom_headers = array();
|
493 |
+
|
494 |
+
/**
|
495 |
+
* The email address being sent from
|
496 |
+
*
|
497 |
+
* @var string
|
498 |
+
*/
|
499 |
+
private $from_email = NULL;
|
500 |
+
|
501 |
+
/**
|
502 |
+
* The HTML body of the email
|
503 |
+
*
|
504 |
+
* @var string
|
505 |
+
*/
|
506 |
+
private $html_body = NULL;
|
507 |
+
|
508 |
+
/**
|
509 |
+
* The Message-ID header for the email
|
510 |
+
*
|
511 |
+
* @var string
|
512 |
+
*/
|
513 |
+
private $message_id = NULL;
|
514 |
+
|
515 |
+
/**
|
516 |
+
* The plaintext body of the email
|
517 |
+
*
|
518 |
+
* @var string
|
519 |
+
*/
|
520 |
+
private $plaintext_body = NULL;
|
521 |
+
|
522 |
+
/**
|
523 |
+
* The recipient's S/MIME PEM certificate filename, used for encryption of the message
|
524 |
+
*
|
525 |
+
* @var string
|
526 |
+
*/
|
527 |
+
private $recipients_smime_cert_file = NULL;
|
528 |
+
|
529 |
+
/**
|
530 |
+
* The files to include as multipart/related
|
531 |
+
*
|
532 |
+
* @var array
|
533 |
+
*/
|
534 |
+
private $related_files = array();
|
535 |
+
|
536 |
+
/**
|
537 |
+
* The email address to reply to
|
538 |
+
*
|
539 |
+
* @var string
|
540 |
+
*/
|
541 |
+
private $reply_to_email = NULL;
|
542 |
+
|
543 |
+
/**
|
544 |
+
* The email address actually sending the email
|
545 |
+
*
|
546 |
+
* @var string
|
547 |
+
*/
|
548 |
+
private $sender_email = NULL;
|
549 |
+
|
550 |
+
/**
|
551 |
+
* The senders's S/MIME PEM certificate filename, used for singing the message
|
552 |
+
*
|
553 |
+
* @var string
|
554 |
+
*/
|
555 |
+
private $senders_smime_cert_file = NULL;
|
556 |
+
|
557 |
+
/**
|
558 |
+
* The senders's S/MIME private key filename, used for singing the message
|
559 |
+
*
|
560 |
+
* @var string
|
561 |
+
*/
|
562 |
+
private $senders_smime_pk_file = NULL;
|
563 |
+
|
564 |
+
/**
|
565 |
+
* The senders's S/MIME private key password, used for singing the message
|
566 |
+
*
|
567 |
+
* @var string
|
568 |
+
*/
|
569 |
+
private $senders_smime_pk_password = NULL;
|
570 |
+
|
571 |
+
/**
|
572 |
+
* If the message should be encrypted using the recipient's S/MIME certificate
|
573 |
+
*
|
574 |
+
* @var boolean
|
575 |
+
*/
|
576 |
+
private $smime_encrypt = FALSE;
|
577 |
+
|
578 |
+
/**
|
579 |
+
* If the message should be signed using the senders's S/MIME private key
|
580 |
+
*
|
581 |
+
* @var boolean
|
582 |
+
*/
|
583 |
+
private $smime_sign = FALSE;
|
584 |
+
|
585 |
+
/**
|
586 |
+
* The subject of the email
|
587 |
+
*
|
588 |
+
* @var string
|
589 |
+
*/
|
590 |
+
private $subject = NULL;
|
591 |
+
|
592 |
+
/**
|
593 |
+
* The email address(es) to send to
|
594 |
+
*
|
595 |
+
* @var array
|
596 |
+
*/
|
597 |
+
private $to_emails = array();
|
598 |
+
|
599 |
+
/**
|
600 |
+
* Initializes fEmail for creating message ids
|
601 |
+
*
|
602 |
+
* @return fEmail
|
603 |
+
*/
|
604 |
+
public function __construct() {
|
605 |
+
$this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
|
606 |
+
}
|
607 |
+
|
608 |
+
/**
|
609 |
+
* All requests that hit this method should be requests for callbacks
|
610 |
+
*
|
611 |
+
* @internal
|
612 |
+
*
|
613 |
+
* @param string $method The method to create a callback for
|
614 |
+
* @return callback The callback for the method requested
|
615 |
+
*/
|
616 |
+
public function __get($method) {
|
617 |
+
return array($this, $method);
|
618 |
+
}
|
619 |
+
|
620 |
+
/**
|
621 |
+
* Adds an attachment to the email
|
622 |
+
*
|
623 |
+
* If a duplicate filename is detected, it will be changed to be unique.
|
624 |
+
*
|
625 |
+
* @param string|fFile $contents The contents of the file
|
626 |
+
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
|
627 |
+
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
|
628 |
+
* @return fEmail The email object, to allow for method chaining
|
629 |
+
*/
|
630 |
+
public function addAttachment($contents, $filename = NULL, $mime_type = NULL) {
|
631 |
+
$this->extrapolateFileInfo($contents, $filename, $mime_type);
|
632 |
+
|
633 |
+
while (isset($this->attachments[$filename])) {
|
634 |
+
$filename = $this->generateNewFilename($filename);
|
635 |
+
}
|
636 |
+
|
637 |
+
$this->attachments[$filename] = array(
|
638 |
+
'mime-type' => $mime_type,
|
639 |
+
'contents' => $contents
|
640 |
+
);
|
641 |
+
|
642 |
+
return $this;
|
643 |
+
}
|
644 |
+
|
645 |
+
/**
|
646 |
+
* Adds a “related” file to the email, returning the `Content-ID` for use in HTML
|
647 |
+
*
|
648 |
+
* The purpose of a related file is to be able to reference it in part of
|
649 |
+
* the HTML body. Image `src` URLs can reference a related file by starting
|
650 |
+
* the URL with `cid:` and then inserting the `Content-ID`.
|
651 |
+
*
|
652 |
+
* If a duplicate filename is detected, it will be changed to be unique.
|
653 |
+
*
|
654 |
+
* @param string|fFile $contents The contents of the file
|
655 |
+
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
|
656 |
+
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
|
657 |
+
* @return string The fully-formed `cid:` URL for use in HTML `src` attributes
|
658 |
+
*/
|
659 |
+
public function addRelatedFile($contents, $filename = NULL, $mime_type = NULL) {
|
660 |
+
$this->extrapolateFileInfo($contents, $filename, $mime_type);
|
661 |
+
|
662 |
+
while (isset($this->related_files[$filename])) {
|
663 |
+
$filename = $this->generateNewFilename($filename);
|
664 |
+
}
|
665 |
+
|
666 |
+
$cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);
|
667 |
+
|
668 |
+
$this->related_files[$filename] = array(
|
669 |
+
'mime-type' => $mime_type,
|
670 |
+
'contents' => $contents,
|
671 |
+
'content-id' => '<' . $cid . '>'
|
672 |
+
);
|
673 |
+
|
674 |
+
return 'cid:' . $cid;
|
675 |
+
}
|
676 |
+
|
677 |
+
/**
|
678 |
+
* Adds a blind carbon copy (BCC) email recipient
|
679 |
+
*
|
680 |
+
* @param string $email The email address to BCC
|
681 |
+
* @param string $name The recipient's name
|
682 |
+
* @return fEmail The email object, to allow for method chaining
|
683 |
+
*/
|
684 |
+
public function addBCCRecipient($email, $name = NULL) {
|
685 |
+
if (!$email) {
|
686 |
+
return;
|
687 |
+
}
|
688 |
+
|
689 |
+
$this->bcc_emails[] = self::combineNameEmail($name, $email);
|
690 |
+
|
691 |
+
return $this;
|
692 |
+
}
|
693 |
+
|
694 |
+
/**
|
695 |
+
* Adds a carbon copy (CC) email recipient
|
696 |
+
*
|
697 |
+
* @param string $email The email address to BCC
|
698 |
+
* @param string $name The recipient's name
|
699 |
+
* @return fEmail The email object, to allow for method chaining
|
700 |
+
*/
|
701 |
+
public function addCCRecipient($email, $name = NULL) {
|
702 |
+
if (!$email) {
|
703 |
+
return;
|
704 |
+
}
|
705 |
+
|
706 |
+
$this->cc_emails[] = self::combineNameEmail($name, $email);
|
707 |
+
|
708 |
+
return $this;
|
709 |
+
}
|
710 |
+
|
711 |
+
/**
|
712 |
+
* Allows adding a custom header to the email
|
713 |
+
*
|
714 |
+
* If the method is called multiple times with the same name, the last
|
715 |
+
* value will be used.
|
716 |
+
*
|
717 |
+
* Please note that this class will properly format the header, including
|
718 |
+
* adding the `:` between the name and value and wrapping values that are
|
719 |
+
* too long for a single line.
|
720 |
+
*
|
721 |
+
* @param string $name The name of the header
|
722 |
+
* @param string $value The value of the header
|
723 |
+
* @param array :$headers An associative array of `{name} => {value}`
|
724 |
+
* @return fEmail The email object, to allow for method chaining
|
725 |
+
*/
|
726 |
+
public function addCustomHeader($name, $value = NULL) {
|
727 |
+
if ($value === NULL && is_array($name)) {
|
728 |
+
foreach ($name as $key => $value) {
|
729 |
+
$this->addCustomHeader($key, $value);
|
730 |
+
}
|
731 |
+
return;
|
732 |
+
}
|
733 |
+
|
734 |
+
$lower_name = fUTF8::lower($name);
|
735 |
+
$this->custom_headers[$lower_name] = array($name, $value);
|
736 |
+
|
737 |
+
return $this;
|
738 |
+
}
|
739 |
+
|
740 |
+
/**
|
741 |
+
* Adds an email recipient
|
742 |
+
*
|
743 |
+
* @param string $email The email address to send to
|
744 |
+
* @param string $name The recipient's name
|
745 |
+
* @return fEmail The email object, to allow for method chaining
|
746 |
+
*/
|
747 |
+
public function addRecipient($email, $name = NULL) {
|
748 |
+
if (!$email) {
|
749 |
+
return;
|
750 |
+
}
|
751 |
+
|
752 |
+
$this->to_emails[] = self::combineNameEmail($name, $email);
|
753 |
+
|
754 |
+
return $this;
|
755 |
+
}
|
756 |
+
|
757 |
+
/**
|
758 |
+
* Takes a multi-address email header and builds it out using an array of emails
|
759 |
+
*
|
760 |
+
* @param string $header The header name without `': '`, the header is non-blank, `': '` will be added
|
761 |
+
* @param array $emails The email addresses for the header
|
762 |
+
* @return string The email header with a trailing `\r\n`
|
763 |
+
*/
|
764 |
+
private function buildMultiAddressHeader($header, $emails) {
|
765 |
+
$header .= ': ';
|
766 |
+
|
767 |
+
$first = TRUE;
|
768 |
+
$line = 1;
|
769 |
+
foreach ($emails as $email) {
|
770 |
+
if ($first) {
|
771 |
+
$first = FALSE;
|
772 |
+
} else {
|
773 |
+
$header .= ', ';
|
774 |
+
}
|
775 |
+
|
776 |
+
// Try to stay within the recommended 78 character line limit
|
777 |
+
$last_crlf_pos = (integer) strrpos($header, "\r\n");
|
778 |
+
if (strlen($header . $email) - $last_crlf_pos > 78) {
|
779 |
+
$header .= "\r\n ";
|
780 |
+
$line++;
|
781 |
+
}
|
782 |
+
|
783 |
+
$header .= trim($email);
|
784 |
+
}
|
785 |
+
|
786 |
+
return $header . "\r\n";
|
787 |
+
}
|
788 |
+
|
789 |
+
/**
|
790 |
+
* Removes all To, CC and BCC recipients from the email
|
791 |
+
*
|
792 |
+
* @return fEmail The email object, to allow for method chaining
|
793 |
+
*/
|
794 |
+
public function clearRecipients() {
|
795 |
+
$this->to_emails = array();
|
796 |
+
$this->cc_emails = array();
|
797 |
+
$this->bcc_emails = array();
|
798 |
+
|
799 |
+
return $this;
|
800 |
+
}
|
801 |
+
|
802 |
+
/**
|
803 |
+
* Creates a 32-character boundary for a multipart message
|
804 |
+
*
|
805 |
+
* @return string A multipart boundary
|
806 |
+
*/
|
807 |
+
private function createBoundary() {
|
808 |
+
$chars = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
|
809 |
+
$last_index = strlen($chars) - 1;
|
810 |
+
$output = '';
|
811 |
+
|
812 |
+
for ($i = 0; $i < 28; $i++) {
|
813 |
+
$output .= $chars[rand(0, $last_index)];
|
814 |
+
}
|
815 |
+
return $output;
|
816 |
+
}
|
817 |
+
|
818 |
+
/**
|
819 |
+
* Builds the body of the email
|
820 |
+
*
|
821 |
+
* @param string $boundary The boundary to use for the top level mime block
|
822 |
+
* @return string The message body to be sent to the mail() function
|
823 |
+
*/
|
824 |
+
private function createBody($boundary) {
|
825 |
+
$boundary_stack = array($boundary);
|
826 |
+
|
827 |
+
$mime_notice = self::compose(
|
828 |
+
"This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
|
829 |
+
);
|
830 |
+
|
831 |
+
$body = '';
|
832 |
+
|
833 |
+
if ($this->html_body || $this->attachments) {
|
834 |
+
$body .= $mime_notice . "\r\n\r\n";
|
835 |
+
}
|
836 |
+
|
837 |
+
if ($this->html_body && $this->related_files && $this->attachments) {
|
838 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
839 |
+
$boundary_stack[] = $this->createBoundary();
|
840 |
+
$body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
|
841 |
+
}
|
842 |
+
|
843 |
+
if ($this->html_body && ($this->attachments || $this->related_files)) {
|
844 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
845 |
+
$boundary_stack[] = $this->createBoundary();
|
846 |
+
$body .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
|
847 |
+
}
|
848 |
+
|
849 |
+
if ($this->html_body || $this->attachments) {
|
850 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
851 |
+
$body .= "Content-Type: text/plain; charset=utf-8\r\n";
|
852 |
+
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
|
853 |
+
}
|
854 |
+
|
855 |
+
$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
|
856 |
+
|
857 |
+
if ($this->html_body) {
|
858 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
859 |
+
$body .= "Content-Type: text/html; charset=utf-8\r\n";
|
860 |
+
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
|
861 |
+
$body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
|
862 |
+
}
|
863 |
+
|
864 |
+
if ($this->related_files) {
|
865 |
+
$body .= '--' . end($boundary_stack) . "--\r\n";
|
866 |
+
array_pop($boundary_stack);
|
867 |
+
|
868 |
+
foreach ($this->related_files as $filename => $file_info) {
|
869 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
870 |
+
$body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
|
871 |
+
$body .= "Content-Transfer-Encoding: base64\r\n";
|
872 |
+
$body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
|
873 |
+
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
|
874 |
+
}
|
875 |
+
}
|
876 |
+
|
877 |
+
if ($this->attachments) {
|
878 |
+
|
879 |
+
if ($this->html_body) {
|
880 |
+
$body .= '--' . end($boundary_stack) . "--\r\n";
|
881 |
+
array_pop($boundary_stack);
|
882 |
+
}
|
883 |
+
|
884 |
+
foreach ($this->attachments as $filename => $file_info) {
|
885 |
+
$body .= '--' . end($boundary_stack) . "\r\n";
|
886 |
+
$body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
|
887 |
+
$body .= "Content-Transfer-Encoding: base64\r\n";
|
888 |
+
$body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
|
889 |
+
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
|
890 |
+
}
|
891 |
+
}
|
892 |
+
|
893 |
+
if ($this->html_body || $this->attachments) {
|
894 |
+
$body .= '--' . end($boundary_stack) . "--\r\n";
|
895 |
+
array_pop($boundary_stack);
|
896 |
+
}
|
897 |
+
|
898 |
+
return $body;
|
899 |
+
}
|
900 |
+
|
901 |
+
/**
|
902 |
+
* Builds the headers for the email
|
903 |
+
*
|
904 |
+
* @param string $boundary The boundary to use for the top level mime block
|
905 |
+
* @param string $message_id The message id for the message
|
906 |
+
* @return string The headers to be sent to the [http://php.net/function.mail mail()] function
|
907 |
+
*/
|
908 |
+
private function createHeaders($boundary, $message_id) {
|
909 |
+
$headers = '';
|
910 |
+
|
911 |
+
if ($this->cc_emails) {
|
912 |
+
$headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
|
913 |
+
}
|
914 |
+
|
915 |
+
if ($this->bcc_emails) {
|
916 |
+
$headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
|
917 |
+
}
|
918 |
+
|
919 |
+
$headers .= "From: " . trim($this->from_email) . "\r\n";
|
920 |
+
|
921 |
+
if ($this->reply_to_email) {
|
922 |
+
$headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
|
923 |
+
}
|
924 |
+
|
925 |
+
if ($this->sender_email) {
|
926 |
+
$headers .= "Sender: " . trim($this->sender_email) . "\r\n";
|
927 |
+
}
|
928 |
+
|
929 |
+
foreach ($this->custom_headers as $header_info) {
|
930 |
+
$header_prefix = $header_info[0] . ': ';
|
931 |
+
$headers .= $header_prefix . self::makeEncodedWord($header_info[1], strlen($header_prefix)) . "\r\n";
|
932 |
+
}
|
933 |
+
|
934 |
+
$headers .= "Message-ID: " . $message_id . "\r\n";
|
935 |
+
$headers .= "MIME-Version: 1.0\r\n";
|
936 |
+
|
937 |
+
if (!$this->html_body && !$this->attachments) {
|
938 |
+
$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
|
939 |
+
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
|
940 |
+
} elseif ($this->html_body && !$this->attachments) {
|
941 |
+
if ($this->related_files) {
|
942 |
+
$headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
|
943 |
+
} else {
|
944 |
+
$headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
|
945 |
+
}
|
946 |
+
} elseif ($this->attachments) {
|
947 |
+
$headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
|
948 |
+
}
|
949 |
+
|
950 |
+
return $headers . "\r\n";
|
951 |
+
}
|
952 |
+
|
953 |
+
/**
|
954 |
+
* Takes the body of the message and processes it with S/MIME
|
955 |
+
*
|
956 |
+
* @param string $to The recipients being sent to
|
957 |
+
* @param string $subject The subject of the email
|
958 |
+
* @param string $headers The headers for the message
|
959 |
+
* @param string $body The message body
|
960 |
+
* @return array `0` => The message headers, `1` => The message body
|
961 |
+
*/
|
962 |
+
private function createSMIMEBody($to, $subject, $headers, $body) {
|
963 |
+
if (!$this->smime_encrypt && !$this->smime_sign) {
|
964 |
+
return array($headers, $body);
|
965 |
+
}
|
966 |
+
|
967 |
+
$plaintext_file = tempnam('', '__fEmail_');
|
968 |
+
$ciphertext_file = tempnam('', '__fEmail_');
|
969 |
+
|
970 |
+
$headers_array = array(
|
971 |
+
'To' => $to,
|
972 |
+
'Subject' => $subject
|
973 |
+
);
|
974 |
+
|
975 |
+
preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
|
976 |
+
foreach ($header_matches as $header_match) {
|
977 |
+
$headers_array[$header_match[1]] = trim($header_match[2]);
|
978 |
+
}
|
979 |
+
|
980 |
+
$body_headers = "";
|
981 |
+
if (isset($headers_array['Content-Type'])) {
|
982 |
+
$body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
|
983 |
+
}
|
984 |
+
if (isset($headers_array['Content-Transfer-Encoding'])) {
|
985 |
+
$body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
|
986 |
+
}
|
987 |
+
|
988 |
+
if ($body_headers) {
|
989 |
+
$body = $body_headers . "\r\n" . $body;
|
990 |
+
}
|
991 |
+
|
992 |
+
file_put_contents($plaintext_file, $body);
|
993 |
+
file_put_contents($ciphertext_file, '');
|
994 |
+
|
995 |
+
// Set up the neccessary S/MIME resources
|
996 |
+
if ($this->smime_sign) {
|
997 |
+
$senders_smime_cert = file_get_contents($this->senders_smime_cert_file);
|
998 |
+
$senders_private_key = openssl_pkey_get_private(
|
999 |
+
file_get_contents($this->senders_smime_pk_file), $this->senders_smime_pk_password
|
1000 |
+
);
|
1001 |
+
|
1002 |
+
if ($senders_private_key === FALSE) {
|
1003 |
+
throw new fValidationException("The sender's S/MIME private key password specified does not appear to be valid for the private key");
|
1004 |
+
}
|
1005 |
+
}
|
1006 |
+
|
1007 |
+
if ($this->smime_encrypt) {
|
1008 |
+
$recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
|
1009 |
+
}
|
1010 |
+
|
1011 |
+
|
1012 |
+
// If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
|
1013 |
+
if ($this->smime_encrypt && $this->smime_sign) {
|
1014 |
+
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
|
1015 |
+
openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
|
1016 |
+
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
|
1017 |
+
} elseif ($this->smime_sign) {
|
1018 |
+
openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
|
1019 |
+
} elseif ($this->smime_encrypt) {
|
1020 |
+
openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
|
1021 |
+
}
|
1022 |
+
|
1023 |
+
// It seems that the contents of the ciphertext is not always \r\n line breaks
|
1024 |
+
$message = file_get_contents($ciphertext_file);
|
1025 |
+
$message = str_replace("\r\n", "\n", $message);
|
1026 |
+
$message = str_replace("\r", "\n", $message);
|
1027 |
+
$message = str_replace("\n", "\r\n", $message);
|
1028 |
+
|
1029 |
+
list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
|
1030 |
+
|
1031 |
+
$new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
|
1032 |
+
$new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
|
1033 |
+
$new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
|
1034 |
+
$new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
|
1035 |
+
$new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
|
1036 |
+
|
1037 |
+
unlink($plaintext_file);
|
1038 |
+
unlink($ciphertext_file);
|
1039 |
+
|
1040 |
+
if ($this->smime_sign) {
|
1041 |
+
openssl_pkey_free($senders_private_key);
|
1042 |
+
}
|
1043 |
+
|
1044 |
+
return array($new_headers, $new_body);
|
1045 |
+
}
|
1046 |
+
|
1047 |
+
/**
|
1048 |
+
* Sets the email to be encrypted with S/MIME
|
1049 |
+
*
|
1050 |
+
* @param string $recipients_smime_cert_file The file path to the PEM-encoded S/MIME certificate for the recipient
|
1051 |
+
* @return fEmail The email object, to allow for method chaining
|
1052 |
+
*/
|
1053 |
+
public function encrypt($recipients_smime_cert_file) {
|
1054 |
+
if (!extension_loaded('openssl')) {
|
1055 |
+
throw new fEnvironmentException('S/MIME encryption was requested for an email, but the %s extension is not installed', 'openssl');
|
1056 |
+
}
|
1057 |
+
|
1058 |
+
if (!self::stringlike($recipients_smime_cert_file)) {
|
1059 |
+
throw new fProgrammerException("The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename", $recipients_smime_cert_file);
|
1060 |
+
}
|
1061 |
+
|
1062 |
+
$this->smime_encrypt = TRUE;
|
1063 |
+
$this->recipients_smime_cert_file = $recipients_smime_cert_file;
|
1064 |
+
|
1065 |
+
return $this;
|
1066 |
+
}
|
1067 |
+
|
1068 |
+
/**
|
1069 |
+
* Extracts just the email addresses from an array of strings containing an
|
1070 |
+
* <email@address.com> or "Name" <email@address.com> combination.
|
1071 |
+
*
|
1072 |
+
* @param array $list The list of email or name/email to extract from
|
1073 |
+
* @return array The email addresses
|
1074 |
+
*/
|
1075 |
+
private function extractEmails($list) {
|
1076 |
+
$output = array();
|
1077 |
+
foreach ($list as $email) {
|
1078 |
+
if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
|
1079 |
+
$output[] = $match[2];
|
1080 |
+
} else {
|
1081 |
+
preg_match(self::EMAIL_REGEX, $email, $match);
|
1082 |
+
$output[] = $match[0];
|
1083 |
+
}
|
1084 |
+
}
|
1085 |
+
return $output;
|
1086 |
+
}
|
1087 |
+
|
1088 |
+
/**
|
1089 |
+
* Extracts the filename and mime-type from an fFile object
|
1090 |
+
*
|
1091 |
+
* @param string|fFile &$contents The file to extrapolate the info from
|
1092 |
+
* @param string &$filename The filename to use for the file
|
1093 |
+
* @param string &$mime_type The mime type of the file
|
1094 |
+
* @return void
|
1095 |
+
*/
|
1096 |
+
private function extrapolateFileInfo(&$contents, &$filename, &$mime_type) {
|
1097 |
+
if ($contents instanceof fFile) {
|
1098 |
+
if ($filename === NULL) {
|
1099 |
+
$filename = $contents->getName();
|
1100 |
+
}
|
1101 |
+
if ($mime_type === NULL) {
|
1102 |
+
$mime_type = $contents->getMimeType();
|
1103 |
+
}
|
1104 |
+
$contents = $contents->read();
|
1105 |
+
} else {
|
1106 |
+
if (!self::stringlike($filename)) {
|
1107 |
+
throw new fProgrammerException('The filename specified, %s, does not appear to be a valid filename', $filename);
|
1108 |
+
}
|
1109 |
+
|
1110 |
+
$filename = (string) $filename;
|
1111 |
+
|
1112 |
+
if ($mime_type === NULL) {
|
1113 |
+
$mime_type = fFile::determineMimeType($filename, $contents);
|
1114 |
+
}
|
1115 |
+
}
|
1116 |
+
}
|
1117 |
+
|
1118 |
+
/**
|
1119 |
+
* Generates a new filename in an attempt to create a unique name
|
1120 |
+
*
|
1121 |
+
* @param string $filename The filename to generate another name for
|
1122 |
+
* @return string The newly generated filename
|
1123 |
+
*/
|
1124 |
+
private function generateNewFilename($filename) {
|
1125 |
+
$filename_info = fFilesystem::getPathInfo($filename);
|
1126 |
+
if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
|
1127 |
+
$i = $match[1] + 1;
|
1128 |
+
} else {
|
1129 |
+
$i = 1;
|
1130 |
+
}
|
1131 |
+
$extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
|
1132 |
+
return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
|
1133 |
+
}
|
1134 |
+
|
1135 |
+
/**
|
1136 |
+
* Loads the plaintext version of the email body from a file and applies replacements
|
1137 |
+
*
|
1138 |
+
* The should contain either ASCII or UTF-8 encoded text. Please see
|
1139 |
+
* http://flourishlib.com/docs/UTF-8 for more information.
|
1140 |
+
*
|
1141 |
+
* @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
|
1142 |
+
*
|
1143 |
+
* @param string|fFile $file The plaintext version of the email body
|
1144 |
+
* @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
|
1145 |
+
* @return fEmail The email object, to allow for method chaining
|
1146 |
+
*/
|
1147 |
+
public function loadBody($file, $replacements = array()) {
|
1148 |
+
if (!$file instanceof fFile) {
|
1149 |
+
$file = new fFile($file);
|
1150 |
+
}
|
1151 |
+
|
1152 |
+
$plaintext = $file->read();
|
1153 |
+
if ($replacements) {
|
1154 |
+
$plaintext = strtr($plaintext, $replacements);
|
1155 |
+
}
|
1156 |
+
|
1157 |
+
$this->plaintext_body = $plaintext;
|
1158 |
+
|
1159 |
+
return $this;
|
1160 |
+
}
|
1161 |
+
|
1162 |
+
/**
|
1163 |
+
* Loads the plaintext version of the email body from a file and applies replacements
|
1164 |
+
*
|
1165 |
+
* The should contain either ASCII or UTF-8 encoded text. Please see
|
1166 |
+
* http://flourishlib.com/docs/UTF-8 for more information.
|
1167 |
+
*
|
1168 |
+
* @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
|
1169 |
+
*
|
1170 |
+
* @param string|fFile $file The plaintext version of the email body
|
1171 |
+
* @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
|
1172 |
+
* @return fEmail The email object, to allow for method chaining
|
1173 |
+
*/
|
1174 |
+
public function loadHTMLBody($file, $replacements = array()) {
|
1175 |
+
if (!$file instanceof fFile) {
|
1176 |
+
$file = new fFile($file);
|
1177 |
+
}
|
1178 |
+
|
1179 |
+
$html = $file->read();
|
1180 |
+
if ($replacements) {
|
1181 |
+
$html = strtr($html, $replacements);
|
1182 |
+
}
|
1183 |
+
|
1184 |
+
$this->html_body = $html;
|
1185 |
+
|
1186 |
+
return $this;
|
1187 |
+
}
|
1188 |
+
|
1189 |
+
/**
|
1190 |
+
* Encodes a string to base64
|
1191 |
+
*
|
1192 |
+
* @param string $content The content to encode
|
1193 |
+
* @return string The encoded string
|
1194 |
+
*/
|
1195 |
+
private function makeBase64($content) {
|
1196 |
+
return chunk_split(base64_encode($content));
|
1197 |
+
}
|
1198 |
+
|
1199 |
+
/**
|
1200 |
+
* Encodes a string to quoted-printable, properly handles UTF-8
|
1201 |
+
*
|
1202 |
+
* @param string $content The content to encode
|
1203 |
+
* @return string The encoded string
|
1204 |
+
*/
|
1205 |
+
private function makeQuotedPrintable($content) {
|
1206 |
+
// Homogenize the line-endings to CRLF
|
1207 |
+
$content = str_replace("\r\n", "\n", $content);
|
1208 |
+
$content = str_replace("\r", "\n", $content);
|
1209 |
+
$content = str_replace("\n", "\r\n", $content);
|
1210 |
+
|
1211 |
+
// A quick a dirty hex encoding
|
1212 |
+
$content = rawurlencode($content);
|
1213 |
+
$content = str_replace('=', '%3D', $content);
|
1214 |
+
$content = str_replace('%', '=', $content);
|
1215 |
+
|
1216 |
+
// Decode characters that don't have to be coded
|
1217 |
+
$decodings = array(
|
1218 |
+
'=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
|
1219 |
+
'=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
|
1220 |
+
'=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
|
1221 |
+
'=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
|
1222 |
+
'=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
|
1223 |
+
'=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
|
1224 |
+
'=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
|
1225 |
+
'=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
|
1226 |
+
);
|
1227 |
+
|
1228 |
+
$content = strtr($content, $decodings);
|
1229 |
+
|
1230 |
+
$output = '';
|
1231 |
+
|
1232 |
+
$length = strlen($content);
|
1233 |
+
|
1234 |
+
// This loop goes through and ensures we are wrapping by 76 chars
|
1235 |
+
$line_length = 0;
|
1236 |
+
for ($i = 0; $i < $length; $i++) {
|
1237 |
+
|
1238 |
+
// Get info about the next character
|
1239 |
+
$char_length = ($content[$i] == '=') ? 3 : 1;
|
1240 |
+
$char = $content[$i];
|
1241 |
+
if ($char_length == 3) {
|
1242 |
+
$char .= $content[$i + 1] . $content[$i + 2];
|
1243 |
+
}
|
1244 |
+
|
1245 |
+
// Skip characters if we have an encoded character, this must be
|
1246 |
+
// done before checking for whitespace at the beginning and end of
|
1247 |
+
// lines or else characters in the content will be skipped
|
1248 |
+
$i += $char_length - 1;
|
1249 |
+
|
1250 |
+
// Spaces and tabs at the beginning and ending of lines have to be encoded
|
1251 |
+
$begining_or_end = $line_length > 69 || $line_length == 0;
|
1252 |
+
$tab_or_space = $char == ' ' || $char == "\t";
|
1253 |
+
if ($begining_or_end && $tab_or_space) {
|
1254 |
+
$char_length = 3;
|
1255 |
+
$char = ($char == ' ') ? '=20' : '=09';
|
1256 |
+
}
|
1257 |
+
|
1258 |
+
// If we have too long a line, wrap it
|
1259 |
+
if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
|
1260 |
+
$output .= "=\r\n";
|
1261 |
+
$line_length = 0;
|
1262 |
+
}
|
1263 |
+
|
1264 |
+
// Add the character
|
1265 |
+
$output .= $char;
|
1266 |
+
|
1267 |
+
// Figure out how much longer the line is now
|
1268 |
+
if ($char == "\r" || $char == "\n") {
|
1269 |
+
$line_length = 0;
|
1270 |
+
} else {
|
1271 |
+
$line_length += $char_length;
|
1272 |
+
}
|
1273 |
+
}
|
1274 |
+
|
1275 |
+
return $output;
|
1276 |
+
}
|
1277 |
+
|
1278 |
+
/**
|
1279 |
+
* Sends the email
|
1280 |
+
*
|
1281 |
+
* The return value is the message id, which should be included as the
|
1282 |
+
* `Message-ID` header of the email. While almost all SMTP servers will not
|
1283 |
+
* modify this value, testing has indicated at least one (smtp.live.com
|
1284 |
+
* for Windows Live Mail) does.
|
1285 |
+
*
|
1286 |
+
* @throws fValidationException When ::validate() throws an exception
|
1287 |
+
*
|
1288 |
+
* @param fSMTP $connection The SMTP connection to send the message over
|
1289 |
+
* @return string The message id for the message - see method description for details
|
1290 |
+
*/
|
1291 |
+
public function send($connection = NULL) {
|
1292 |
+
$this->validate();
|
1293 |
+
|
1294 |
+
// The mail() function on Windows doesn't support names in headers so
|
1295 |
+
// we must strip them down to just the email address
|
1296 |
+
if ($connection === NULL && fCore::checkOS('windows')) {
|
1297 |
+
$vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
|
1298 |
+
foreach ($vars as $var) {
|
1299 |
+
if (!is_array($this->$var)) {
|
1300 |
+
if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
|
1301 |
+
$this->$var = $match[2];
|
1302 |
+
}
|
1303 |
+
} else {
|
1304 |
+
$new_emails = array();
|
1305 |
+
foreach ($this->$var as $email) {
|
1306 |
+
if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
|
1307 |
+
$email = $match[2];
|
1308 |
+
}
|
1309 |
+
$new_emails[] = $email;
|
1310 |
+
}
|
1311 |
+
$this->$var = $new_emails;
|
1312 |
+
}
|
1313 |
+
}
|
1314 |
+
}
|
1315 |
+
|
1316 |
+
$to = substr(trim($this->buildMultiAddressHeader("To", $this->to_emails)), 4);
|
1317 |
+
|
1318 |
+
$top_level_boundary = $this->createBoundary();
|
1319 |
+
$headers = $this->createHeaders($top_level_boundary, $this->message_id);
|
1320 |
+
|
1321 |
+
$subject = str_replace(array("\r", "\n"), '', $this->subject);
|
1322 |
+
$subject = self::makeEncodedWord($subject, 9);
|
1323 |
+
|
1324 |
+
$body = $this->createBody($top_level_boundary);
|
1325 |
+
|
1326 |
+
if ($this->smime_encrypt || $this->smime_sign) {
|
1327 |
+
list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
|
1328 |
+
}
|
1329 |
+
|
1330 |
+
// Remove extra line breaks
|
1331 |
+
$headers = trim($headers);
|
1332 |
+
$body = trim($body);
|
1333 |
+
|
1334 |
+
if ($connection) {
|
1335 |
+
$to_emails = $this->extractEmails($this->to_emails);
|
1336 |
+
$to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
|
1337 |
+
$to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
|
1338 |
+
$from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
|
1339 |
+
$connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
|
1340 |
+
return $this->message_id;
|
1341 |
+
}
|
1342 |
+
|
1343 |
+
// Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
|
1344 |
+
$parameters = NULL;
|
1345 |
+
if (!fCore::checkOS('windows') && $this->bounce_to_email) {
|
1346 |
+
preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
|
1347 |
+
$parameters = '-f ' . $matches[0];
|
1348 |
+
|
1349 |
+
// Windows takes the Return-Path email from the sendmail_from ini setting
|
1350 |
+
} elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
|
1351 |
+
$old_sendmail_from = ini_get('sendmail_from');
|
1352 |
+
preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
|
1353 |
+
ini_set('sendmail_from', $matches[0]);
|
1354 |
+
}
|
1355 |
+
|
1356 |
+
// This is a gross qmail fix that is a last resort
|
1357 |
+
if (self::$popen_sendmail || self::$convert_crlf) {
|
1358 |
+
$to = str_replace("\r\n", "\n", $to);
|
1359 |
+
$subject = str_replace("\r\n", "\n", $subject);
|
1360 |
+
$body = str_replace("\r\n", "\n", $body);
|
1361 |
+
$headers = str_replace("\r\n", "\n", $headers);
|
1362 |
+
}
|
1363 |
+
|
1364 |
+
// If the user is using qmail and wants to try to fix the \r\r\n line break issue
|
1365 |
+
if (self::$popen_sendmail) {
|
1366 |
+
$sendmail_command = ini_get('sendmail_path');
|
1367 |
+
if ($parameters) {
|
1368 |
+
$sendmail_command .= ' ' . $parameters;
|
1369 |
+
}
|
1370 |
+
|
1371 |
+
$sendmail_process = popen($sendmail_command, 'w');
|
1372 |
+
fprintf($sendmail_process, "To: %s\n", $to);
|
1373 |
+
fprintf($sendmail_process, "Subject: %s\n", $subject);
|
1374 |
+
if ($headers) {
|
1375 |
+
fprintf($sendmail_process, "%s\n", $headers);
|
1376 |
+
}
|
1377 |
+
fprintf($sendmail_process, "\n%s\n", $body);
|
1378 |
+
$error = pclose($sendmail_process);
|
1379 |
+
|
1380 |
+
// This is the normal way to send mail
|
1381 |
+
} else {
|
1382 |
+
// On Windows, mail() sends directly to an SMTP server and will
|
1383 |
+
// strip a leading . from the body
|
1384 |
+
if (fCore::checkOS('windows')) {
|
1385 |
+
$body = preg_replace('#^\.#', '..', $body);
|
1386 |
+
}
|
1387 |
+
|
1388 |
+
if ($parameters) {
|
1389 |
+
$error = !mail($to, $subject, $body, $headers, $parameters);
|
1390 |
+
} else {
|
1391 |
+
$error = !mail($to, $subject, $body, $headers);
|
1392 |
+
}
|
1393 |
+
}
|
1394 |
+
|
1395 |
+
if (fCore::checkOS('windows') && $this->bounce_to_email) {
|
1396 |
+
ini_set('sendmail_from', $old_sendmail_from);
|
1397 |
+
}
|
1398 |
+
|
1399 |
+
if ($error) {
|
1400 |
+
throw new fConnectivityException('An error occured while trying to send the email entitled %s', $this->subject);
|
1401 |
+
}
|
1402 |
+
|
1403 |
+
return $this->message_id;
|
1404 |
+
}
|
1405 |
+
|
1406 |
+
/**
|
1407 |
+
* Sets the plaintext version of the email body
|
1408 |
+
*
|
1409 |
+
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1410 |
+
* http://flourishlib.com/docs/UTF-8 for more information.
|
1411 |
+
*
|
1412 |
+
* @param string $plaintext The plaintext version of the email body
|
1413 |
+
* @param boolean $unindent_expand_constants If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
|
1414 |
+
* @return fEmail The email object, to allow for method chaining
|
1415 |
+
*/
|
1416 |
+
public function setBody($plaintext, $unindent_expand_constants = FALSE) {
|
1417 |
+
if ($unindent_expand_constants) {
|
1418 |
+
$plaintext = self::unindentExpand($plaintext);
|
1419 |
+
}
|
1420 |
+
|
1421 |
+
$this->plaintext_body = $plaintext;
|
1422 |
+
|
1423 |
+
return $this;
|
1424 |
+
}
|
1425 |
+
|
1426 |
+
/**
|
1427 |
+
* Adds the email address the email will be bounced to
|
1428 |
+
*
|
1429 |
+
* This email address will be set to the `Return-Path` header.
|
1430 |
+
*
|
1431 |
+
* @param string $email The email address to bounce to
|
1432 |
+
* @return fEmail The email object, to allow for method chaining
|
1433 |
+
*/
|
1434 |
+
public function setBounceToEmail($email) {
|
1435 |
+
if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
|
1436 |
+
throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
|
1437 |
+
}
|
1438 |
+
if (!$email) {
|
1439 |
+
return;
|
1440 |
+
}
|
1441 |
+
|
1442 |
+
$this->bounce_to_email = self::combineNameEmail('', $email);
|
1443 |
+
|
1444 |
+
return $this;
|
1445 |
+
}
|
1446 |
+
|
1447 |
+
/**
|
1448 |
+
* Adds the `From:` email address to the email
|
1449 |
+
*
|
1450 |
+
* @param string $email The email address being sent from
|
1451 |
+
* @param string $name The from email user's name - unfortunately on windows this is ignored
|
1452 |
+
* @return fEmail The email object, to allow for method chaining
|
1453 |
+
*/
|
1454 |
+
public function setFromEmail($email, $name = NULL) {
|
1455 |
+
if (!$email) {
|
1456 |
+
return;
|
1457 |
+
}
|
1458 |
+
|
1459 |
+
$this->from_email = self::combineNameEmail($name, $email);
|
1460 |
+
|
1461 |
+
return $this;
|
1462 |
+
}
|
1463 |
+
|
1464 |
+
/**
|
1465 |
+
* Sets the HTML version of the email body
|
1466 |
+
*
|
1467 |
+
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1468 |
+
* http://flourishlib.com/docs/UTF-8 for more information.
|
1469 |
+
*
|
1470 |
+
* @param string $html The HTML version of the email body
|
1471 |
+
* @return fEmail The email object, to allow for method chaining
|
1472 |
+
*/
|
1473 |
+
public function setHTMLBody($html) {
|
1474 |
+
$this->html_body = $html;
|
1475 |
+
|
1476 |
+
return $this;
|
1477 |
+
}
|
1478 |
+
|
1479 |
+
/**
|
1480 |
+
* Adds the `Reply-To:` email address to the email
|
1481 |
+
*
|
1482 |
+
* @param string $email The email address to reply to
|
1483 |
+
* @param string $name The reply-to email user's name
|
1484 |
+
* @return fEmail The email object, to allow for method chaining
|
1485 |
+
*/
|
1486 |
+
public function setReplyToEmail($email, $name = NULL) {
|
1487 |
+
if (!$email) {
|
1488 |
+
return;
|
1489 |
+
}
|
1490 |
+
|
1491 |
+
$this->reply_to_email = self::combineNameEmail($name, $email);
|
1492 |
+
|
1493 |
+
return $this;
|
1494 |
+
}
|
1495 |
+
|
1496 |
+
/**
|
1497 |
+
* Adds the `Sender:` email address to the email
|
1498 |
+
*
|
1499 |
+
* The `Sender:` header is used to indicate someone other than the `From:`
|
1500 |
+
* address is actually submitting the message to the network.
|
1501 |
+
*
|
1502 |
+
* @param string $email The email address the message is actually being sent from
|
1503 |
+
* @param string $name The sender email user's name
|
1504 |
+
* @return fEmail The email object, to allow for method chaining
|
1505 |
+
*/
|
1506 |
+
public function setSenderEmail($email, $name = NULL) {
|
1507 |
+
if (!$email) {
|
1508 |
+
return;
|
1509 |
+
}
|
1510 |
+
|
1511 |
+
$this->sender_email = self::combineNameEmail($name, $email);
|
1512 |
+
|
1513 |
+
return $this;
|
1514 |
+
}
|
1515 |
+
|
1516 |
+
/**
|
1517 |
+
* Sets the subject of the email
|
1518 |
+
*
|
1519 |
+
* This method accepts either ASCII or UTF-8 encoded text. Please see
|
1520 |
+
* http://flourishlib.com/docs/UTF-8 for more information.
|
1521 |
+
*
|
1522 |
+
* @param string $subject The subject of the email
|
1523 |
+
* @return fEmail The email object, to allow for method chaining
|
1524 |
+
*/
|
1525 |
+
public function setSubject($subject) {
|
1526 |
+
$this->subject = $subject;
|
1527 |
+
|
1528 |
+
return $this;
|
1529 |
+
}
|
1530 |
+
|
1531 |
+
/**
|
1532 |
+
* Sets the email to be signed with S/MIME
|
1533 |
+
*
|
1534 |
+
* @param string $senders_smime_cert_file The file path to the sender's PEM-encoded S/MIME certificate
|
1535 |
+
* @param string $senders_smime_pk_file The file path to the sender's S/MIME private key
|
1536 |
+
* @param string $senders_smime_pk_password The password for the sender's S/MIME private key
|
1537 |
+
* @return fEmail The email object, to allow for method chaining
|
1538 |
+
*/
|
1539 |
+
public function sign($senders_smime_cert_file, $senders_smime_pk_file, $senders_smime_pk_password) {
|
1540 |
+
if (!extension_loaded('openssl')) {
|
1541 |
+
throw new fEnvironmentException('An S/MIME signature was requested for an email, but the %s extension is not installed', 'openssl');
|
1542 |
+
}
|
1543 |
+
|
1544 |
+
if (!self::stringlike($senders_smime_cert_file)) {
|
1545 |
+
throw new fProgrammerException("The sender's S/MIME certificate file specified, %s, does not appear to be a valid filename", $senders_smime_cert_file);
|
1546 |
+
}
|
1547 |
+
if (!file_exists($senders_smime_cert_file) || !is_readable($senders_smime_cert_file)) {
|
1548 |
+
throw new fEnvironmentException("The sender's S/MIME certificate file specified, %s, does not exist or could not be read", $senders_smime_cert_file);
|
1549 |
+
}
|
1550 |
+
|
1551 |
+
if (!self::stringlike($senders_smime_pk_file)) {
|
1552 |
+
throw new fProgrammerException("The sender's S/MIME primary key file specified, %s, does not appear to be a valid filename", $senders_smime_pk_file);
|
1553 |
+
}
|
1554 |
+
if (!file_exists($senders_smime_pk_file) || !is_readable($senders_smime_pk_file)) {
|
1555 |
+
throw new fEnvironmentException("The sender's S/MIME primary key file specified, %s, does not exist or could not be read", $senders_smime_pk_file);
|
1556 |
+
}
|
1557 |
+
|
1558 |
+
$this->smime_sign = TRUE;
|
1559 |
+
$this->senders_smime_cert_file = $senders_smime_cert_file;
|
1560 |
+
$this->senders_smime_pk_file = $senders_smime_pk_file;
|
1561 |
+
$this->senders_smime_pk_password = $senders_smime_pk_password;
|
1562 |
+
|
1563 |
+
return $this;
|
1564 |
+
}
|
1565 |
+
|
1566 |
+
/**
|
1567 |
+
* Validates that all of the parts of the email are valid
|
1568 |
+
*
|
1569 |
+
* @throws fValidationException When part of the email is missing or formatted incorrectly
|
1570 |
+
*
|
1571 |
+
* @return void
|
1572 |
+
*/
|
1573 |
+
private function validate() {
|
1574 |
+
$validation_messages = array();
|
1575 |
+
|
1576 |
+
// Check all multi-address email field
|
1577 |
+
$multi_address_field_list = array(
|
1578 |
+
'to_emails' => self::compose('recipient'),
|
1579 |
+
'cc_emails' => self::compose('CC recipient'),
|
1580 |
+
'bcc_emails' => self::compose('BCC recipient')
|
1581 |
+
);
|
1582 |
+
|
1583 |
+
foreach ($multi_address_field_list as $field => $name) {
|
1584 |
+
foreach ($this->$field as $email) {
|
1585 |
+
if ($email && !preg_match(self::NAME_EMAIL_REGEX, $email) && !preg_match(self::EMAIL_REGEX, $email)) {
|
1586 |
+
$validation_messages[] = htmlspecialchars(self::compose(
|
1587 |
+
'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.', $name, $email
|
1588 |
+
), ENT_QUOTES, 'UTF-8');
|
1589 |
+
}
|
1590 |
+
}
|
1591 |
+
}
|
1592 |
+
|
1593 |
+
// Check all single-address email fields
|
1594 |
+
$single_address_field_list = array(
|
1595 |
+
'from_email' => self::compose('From email address'),
|
1596 |
+
'reply_to_email' => self::compose('Reply-To email address'),
|
1597 |
+
'sender_email' => self::compose('Sender email address'),
|
1598 |
+
'bounce_to_email' => self::compose('Bounce-To email address')
|
1599 |
+
);
|
1600 |
+
|
1601 |
+
foreach ($single_address_field_list as $field => $name) {
|
1602 |
+
if ($this->$field && !preg_match(self::NAME_EMAIL_REGEX, $this->$field) && !preg_match(self::EMAIL_REGEX, $this->$field)) {
|
1603 |
+
$validation_messages[] = htmlspecialchars(self::compose(
|
1604 |
+
'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.', $name, $this->$field
|
1605 |
+
), ENT_QUOTES, 'UTF-8');
|
1606 |
+
}
|
1607 |
+
}
|
1608 |
+
|
1609 |
+
// Make sure the required fields are all set
|
1610 |
+
if (!$this->to_emails) {
|
1611 |
+
$validation_messages[] = self::compose(
|
1612 |
+
"Please provide at least one recipient"
|
1613 |
+
);
|
1614 |
+
}
|
1615 |
+
|
1616 |
+
if (!$this->from_email) {
|
1617 |
+
$validation_messages[] = self::compose(
|
1618 |
+
"Please provide the from email address"
|
1619 |
+
);
|
1620 |
+
}
|
1621 |
+
|
1622 |
+
if (!self::stringlike($this->subject)) {
|
1623 |
+
$validation_messages[] = self::compose(
|
1624 |
+
"Please provide an email subject"
|
1625 |
+
);
|
1626 |
+
}
|
1627 |
+
|
1628 |
+
if (strpos($this->subject, "\n") !== FALSE) {
|
1629 |
+
$validation_messages[] = self::compose(
|
1630 |
+
"The subject contains one or more newline characters"
|
1631 |
+
);
|
1632 |
+
}
|
1633 |
+
|
1634 |
+
if (!self::stringlike($this->plaintext_body)) {
|
1635 |
+
$validation_messages[] = self::compose(
|
1636 |
+
"Please provide a plaintext email body"
|
1637 |
+
);
|
1638 |
+
}
|
1639 |
+
|
1640 |
+
// Make sure the attachments look good
|
1641 |
+
foreach ($this->attachments as $filename => $file_info) {
|
1642 |
+
if (!self::stringlike($file_info['mime-type'])) {
|
1643 |
+
$validation_messages[] = self::compose(
|
1644 |
+
"No mime-type was specified for the attachment %s", $filename
|
1645 |
+
);
|
1646 |
+
}
|
1647 |
+
if (!self::stringlike($file_info['contents'])) {
|
1648 |
+
$validation_messages[] = self::compose(
|
1649 |
+
"The attachment %s appears to be a blank file", $filename
|
1650 |
+
);
|
1651 |
+
}
|
1652 |
+
}
|
1653 |
+
|
1654 |
+
if ($validation_messages) {
|
1655 |
+
throw new fValidationException('The email could not be sent because:', $validation_messages);
|
1656 |
+
}
|
1657 |
+
}
|
1658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1659 |
}
|
1660 |
|
|
|
|
|
1661 |
/**
|
1662 |
* Copyright (c) 2008-2011 Will Bond <will@flourishlib.com>, others
|
1663 |
*
|
lib/fException.php
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<?php
|
|
|
2 |
/**
|
3 |
* An exception that allows for easy l10n, printing, tracing and hooking
|
4 |
*
|
@@ -19,187 +20,171 @@
|
|
19 |
* @changes 1.0.0b2 ::compose() more robustly handles `$components` passed as an array, ::__construct() now detects stray `%` characters [wb, 2009-02-05]
|
20 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
21 |
*/
|
22 |
-
abstract class fException extends Exception
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
* accepted since they are redundant and restrict the non-formatting use of
|
188 |
-
* the `%` sign in exception messages:
|
189 |
-
* - `% 2d`: Using a literal space as a padding character - a space will be used if no padding character is specified
|
190 |
-
* - `%'.d`: Providing a padding character but no width - no padding will be applied without a width
|
191 |
-
*
|
192 |
-
* @param string $message The message for the exception. This accepts a subset of [http://php.net/sprintf `sprintf()`] strings - see method description for more details.
|
193 |
-
* @param mixed $component A string or number to insert into the message
|
194 |
-
* @param mixed ...
|
195 |
-
* @param mixed $code The exception code to set
|
196 |
-
* @return fException
|
197 |
-
*/
|
198 |
-
public function __construct($message='')
|
199 |
-
{
|
200 |
-
$args = array_slice(func_get_args(), 1);
|
201 |
-
$required_args = preg_match_all(
|
202 |
-
'/
|
203 |
(?<!%) # Ensure this is not an escaped %
|
204 |
%( # The leading %
|
205 |
(?:\d+\$)? # Position
|
@@ -207,368 +192,341 @@ abstract class fException extends Exception
|
|
207 |
(?:(?:0|\'.)?-?\d+|-?) # Padding, alignment and width or just alignment
|
208 |
(?:\.\d+)? # Precision
|
209 |
[bcdeufFosxX] # Type
|
210 |
-
)/x',
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
foreach ($list_items as $list_item) {
|
544 |
-
foreach ($matching_array as $match_num => $matching_string) {
|
545 |
-
if (strpos($list_item[1], $matching_string) !== FALSE) {
|
546 |
-
$matched_list_items[$match_num][] = $list_item[0];
|
547 |
-
$found = TRUE;
|
548 |
-
continue 2;
|
549 |
-
}
|
550 |
-
}
|
551 |
-
}
|
552 |
-
|
553 |
-
if (!$found) {
|
554 |
-
$output[] = '';
|
555 |
-
continue;
|
556 |
-
}
|
557 |
-
|
558 |
-
// This merges all of the multi-dimensional arrays back to one so we can do a simple join
|
559 |
-
$merged_list_items = array();
|
560 |
-
foreach ($matched_list_items as $match_num => $matched_items) {
|
561 |
-
$merged_list_items = array_merge($merged_list_items, $matched_items);
|
562 |
-
}
|
563 |
-
|
564 |
-
$output[] = $beginning_html . join("\n", $merged_list_items) . $ending_html;
|
565 |
-
}
|
566 |
-
|
567 |
-
return $output;
|
568 |
-
}
|
569 |
-
}
|
570 |
-
|
571 |
|
|
|
572 |
|
573 |
/**
|
574 |
* Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
|
1 |
<?php
|
2 |
+
|
3 |
/**
|
4 |
* An exception that allows for easy l10n, printing, tracing and hooking
|
5 |
*
|
20 |
* @changes 1.0.0b2 ::compose() more robustly handles `$components` passed as an array, ::__construct() now detects stray `%` characters [wb, 2009-02-05]
|
21 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
22 |
*/
|
23 |
+
abstract class fException extends Exception {
|
24 |
+
|
25 |
+
/**
|
26 |
+
* Callbacks for when exceptions are created
|
27 |
+
*
|
28 |
+
* @var array
|
29 |
+
*/
|
30 |
+
static private $callbacks = array();
|
31 |
+
|
32 |
+
/**
|
33 |
+
* Composes text using fText if loaded
|
34 |
+
*
|
35 |
+
* @param string $message The message to compose
|
36 |
+
* @param mixed $component A string or number to insert into the message
|
37 |
+
* @param mixed ...
|
38 |
+
* @return string The composed and possible translated message
|
39 |
+
*/
|
40 |
+
static protected function compose($message) {
|
41 |
+
$components = array_slice(func_get_args(), 1);
|
42 |
+
|
43 |
+
// Handles components passed as an array
|
44 |
+
if (sizeof($components) == 1 && is_array($components[0])) {
|
45 |
+
$components = $components[0];
|
46 |
+
}
|
47 |
+
|
48 |
+
// If fText is loaded, use it
|
49 |
+
if (class_exists('fText', FALSE)) {
|
50 |
+
return call_user_func_array(
|
51 |
+
array('fText', 'compose'), array($message, $components)
|
52 |
+
);
|
53 |
+
} else {
|
54 |
+
return vsprintf($message, $components);
|
55 |
+
}
|
56 |
+
}
|
57 |
+
|
58 |
+
/**
|
59 |
+
* Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
|
60 |
+
*
|
61 |
+
* The string output format of this method is very similar to the output of
|
62 |
+
* [http://php.net/print_r print_r()] except that the following values
|
63 |
+
* are represented as special strings:
|
64 |
+
*
|
65 |
+
* - `TRUE`: `'{true}'`
|
66 |
+
* - `FALSE`: `'{false}'`
|
67 |
+
* - `NULL`: `'{null}'`
|
68 |
+
* - `''`: `'{empty_string}'`
|
69 |
+
*
|
70 |
+
* @param mixed $data The value to dump
|
71 |
+
* @return string The string representation of the value
|
72 |
+
*/
|
73 |
+
static protected function dump($data) {
|
74 |
+
if (is_bool($data)) {
|
75 |
+
return ($data) ? '{true}' : '{false}';
|
76 |
+
} elseif (is_null($data)) {
|
77 |
+
return '{null}';
|
78 |
+
} elseif ($data === '') {
|
79 |
+
return '{empty_string}';
|
80 |
+
} elseif (is_array($data) || is_object($data)) {
|
81 |
+
|
82 |
+
ob_start();
|
83 |
+
var_dump($data);
|
84 |
+
$output = ob_get_contents();
|
85 |
+
ob_end_clean();
|
86 |
+
|
87 |
+
// Make the var dump more like a print_r
|
88 |
+
$output = preg_replace('#=>\n( )+(?=[a-zA-Z]|&)#m', ' => ', $output);
|
89 |
+
$output = str_replace('string(0) ""', '{empty_string}', $output);
|
90 |
+
$output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
|
91 |
+
$output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
|
92 |
+
$output = preg_replace('#string\(\d+\) "#', '', $output);
|
93 |
+
$output = preg_replace('#"(\n( )*)(?=\[|\})#', '\1', $output);
|
94 |
+
$output = preg_replace('#(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
|
95 |
+
$output = preg_replace('#((?: )+)\["(.*?)"\]#', '\1[\2]', $output);
|
96 |
+
$output = preg_replace('#(?:&)?array\(\d+\) \{\n((?: )*)((?: )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
|
97 |
+
$output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?: )*)((?: )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
|
98 |
+
$output = preg_replace('#^((?: )+)}(?=\n|$)#m', "\\1)\n", $output);
|
99 |
+
$output = substr($output, 0, -2) . ')';
|
100 |
+
|
101 |
+
// Fix indenting issues with the var dump output
|
102 |
+
$output_lines = explode("\n", $output);
|
103 |
+
$new_output = array();
|
104 |
+
$stack = 0;
|
105 |
+
foreach ($output_lines as $line) {
|
106 |
+
if (preg_match('#^((?: )*)([^ ])#', $line, $match)) {
|
107 |
+
$spaces = strlen($match[1]);
|
108 |
+
if ($spaces && $match[2] == '(') {
|
109 |
+
$stack += 1;
|
110 |
+
}
|
111 |
+
$new_output[] = str_pad('', ($spaces) + (4 * $stack)) . $line;
|
112 |
+
if ($spaces && $match[2] == ')') {
|
113 |
+
$stack -= 1;
|
114 |
+
}
|
115 |
+
} else {
|
116 |
+
$new_output[] = str_pad('', ($spaces) + (4 * $stack)) . $line;
|
117 |
+
}
|
118 |
+
}
|
119 |
+
|
120 |
+
return join("\n", $new_output);
|
121 |
+
} else {
|
122 |
+
return (string) $data;
|
123 |
+
}
|
124 |
+
}
|
125 |
+
|
126 |
+
/**
|
127 |
+
* Adds a callback for when certain types of exceptions are created
|
128 |
+
*
|
129 |
+
* The callback will be called when any exception of this class, or any
|
130 |
+
* child class, specified is tossed. A single parameter will be passed
|
131 |
+
* to the callback, which will be the exception object.
|
132 |
+
*
|
133 |
+
* @param callback $callback The callback
|
134 |
+
* @param string $exception_type The type of exception to call the callback for
|
135 |
+
* @return void
|
136 |
+
*/
|
137 |
+
static public function registerCallback($callback, $exception_type = NULL) {
|
138 |
+
if ($exception_type === NULL) {
|
139 |
+
$exception_type = 'fException';
|
140 |
+
}
|
141 |
+
|
142 |
+
if (!isset(self::$callbacks[$exception_type])) {
|
143 |
+
self::$callbacks[$exception_type] = array();
|
144 |
+
}
|
145 |
+
|
146 |
+
if (is_string($callback) && strpos($callback, '::') !== FALSE) {
|
147 |
+
$callback = explode('::', $callback);
|
148 |
+
}
|
149 |
+
|
150 |
+
self::$callbacks[$exception_type][] = $callback;
|
151 |
+
}
|
152 |
+
|
153 |
+
/**
|
154 |
+
* Compares the message matching strings by longest first so that the longest matches are made first
|
155 |
+
*
|
156 |
+
* @param string $a The first string to compare
|
157 |
+
* @param string $b The second string to compare
|
158 |
+
* @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
|
159 |
+
*/
|
160 |
+
static private function sortMatchingArray($a, $b) {
|
161 |
+
return -1 * strnatcmp(strlen($a), strlen($b));
|
162 |
+
}
|
163 |
+
|
164 |
+
/**
|
165 |
+
* Sets the message for the exception, allowing for string interpolation and internationalization
|
166 |
+
*
|
167 |
+
* The `$message` can contain any number of formatting placeholders for
|
168 |
+
* string and number interpolation via [http://php.net/sprintf `sprintf()`].
|
169 |
+
* Any `%` signs that do not appear to be part of a valid formatting
|
170 |
+
* placeholder will be automatically escaped with a second `%`.
|
171 |
+
*
|
172 |
+
* The following aspects of valid `sprintf()` formatting codes are not
|
173 |
+
* accepted since they are redundant and restrict the non-formatting use of
|
174 |
+
* the `%` sign in exception messages:
|
175 |
+
* - `% 2d`: Using a literal space as a padding character - a space will be used if no padding character is specified
|
176 |
+
* - `%'.d`: Providing a padding character but no width - no padding will be applied without a width
|
177 |
+
*
|
178 |
+
* @param string $message The message for the exception. This accepts a subset of [http://php.net/sprintf `sprintf()`] strings - see method description for more details.
|
179 |
+
* @param mixed $component A string or number to insert into the message
|
180 |
+
* @param mixed ...
|
181 |
+
* @param mixed $code The exception code to set
|
182 |
+
* @return fException
|
183 |
+
*/
|
184 |
+
public function __construct($message = '') {
|
185 |
+
$args = array_slice(func_get_args(), 1);
|
186 |
+
$required_args = preg_match_all(
|
187 |
+
'/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
(?<!%) # Ensure this is not an escaped %
|
189 |
%( # The leading %
|
190 |
(?:\d+\$)? # Position
|
192 |
(?:(?:0|\'.)?-?\d+|-?) # Padding, alignment and width or just alignment
|
193 |
(?:\.\d+)? # Precision
|
194 |
[bcdeufFosxX] # Type
|
195 |
+
)/x', $message, $matches
|
196 |
+
);
|
197 |
+
|
198 |
+
// Handle %s that weren't properly escaped
|
199 |
+
$formats = $matches[1];
|
200 |
+
$delimeters = ($formats) ? array_fill(0, sizeof($formats), '#') : array();
|
201 |
+
$lookahead = join(
|
202 |
+
'|', array_map(
|
203 |
+
'preg_quote', $formats, $delimeters
|
204 |
+
)
|
205 |
+
);
|
206 |
+
$lookahead = ($lookahead) ? '|' . $lookahead : '';
|
207 |
+
$message = preg_replace('#(?<!%)%(?!%' . $lookahead . ')#', '%%', $message);
|
208 |
+
|
209 |
+
// If we have an extra argument, it is the exception code
|
210 |
+
$code = NULL;
|
211 |
+
if ($required_args == sizeof($args) - 1) {
|
212 |
+
$code = array_pop($args);
|
213 |
+
}
|
214 |
+
|
215 |
+
if (sizeof($args) != $required_args) {
|
216 |
+
$message = self::compose(
|
217 |
+
'%1$d components were passed to the %2$s constructor, while %3$d were specified in the message', sizeof($args), get_class($this), $required_args
|
218 |
+
);
|
219 |
+
throw new Exception($message);
|
220 |
+
}
|
221 |
+
|
222 |
+
$args = array_map(array('fException', 'dump'), $args);
|
223 |
+
|
224 |
+
parent::__construct(self::compose($message, $args));
|
225 |
+
$this->code = $code;
|
226 |
+
|
227 |
+
foreach (self::$callbacks as $class => $callbacks) {
|
228 |
+
foreach ($callbacks as $callback) {
|
229 |
+
if ($this instanceof $class) {
|
230 |
+
call_user_func($callback, $this);
|
231 |
+
}
|
232 |
+
}
|
233 |
+
}
|
234 |
+
}
|
235 |
+
|
236 |
+
/**
|
237 |
+
* All requests that hit this method should be requests for callbacks
|
238 |
+
*
|
239 |
+
* @internal
|
240 |
+
*
|
241 |
+
* @param string $method The method to create a callback for
|
242 |
+
* @return callback The callback for the method requested
|
243 |
+
*/
|
244 |
+
public function __get($method) {
|
245 |
+
return array($this, $method);
|
246 |
+
}
|
247 |
+
|
248 |
+
/**
|
249 |
+
* Gets the backtrace to currently called exception
|
250 |
+
*
|
251 |
+
* @return string A nicely formatted backtrace to this exception
|
252 |
+
*/
|
253 |
+
public function formatTrace() {
|
254 |
+
$doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
|
255 |
+
$doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
|
256 |
+
|
257 |
+
$backtrace = explode("\n", $this->getTraceAsString());
|
258 |
+
array_unshift($backtrace, $this->file . '(' . $this->line . ')');
|
259 |
+
$backtrace = preg_replace('/^#\d+\s+/', '', $backtrace);
|
260 |
+
$backtrace = str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $backtrace);
|
261 |
+
$backtrace = array_diff($backtrace, array('{main}'));
|
262 |
+
$backtrace = array_reverse($backtrace);
|
263 |
+
|
264 |
+
return join("\n", $backtrace);
|
265 |
+
}
|
266 |
+
|
267 |
+
/**
|
268 |
+
* Returns the CSS class name for printing information about the exception
|
269 |
+
*
|
270 |
+
* @return void
|
271 |
+
*/
|
272 |
+
protected function getCSSClass() {
|
273 |
+
$string = preg_replace('#^f#', '', get_class($this));
|
274 |
+
|
275 |
+
do {
|
276 |
+
$old_string = $string;
|
277 |
+
$string = preg_replace('/([a-zA-Z])([0-9])/', '\1_\2', $string);
|
278 |
+
$string = preg_replace('/([a-z0-9A-Z])([A-Z])/', '\1_\2', $string);
|
279 |
+
} while ($old_string != $string);
|
280 |
+
|
281 |
+
return strtolower($string);
|
282 |
+
}
|
283 |
+
|
284 |
+
/**
|
285 |
+
* Prepares content for output into HTML
|
286 |
+
*
|
287 |
+
* @return string The prepared content
|
288 |
+
*/
|
289 |
+
protected function prepare($content) {
|
290 |
+
// See if the message has newline characters but not br tags, extracted from fHTML to reduce dependencies
|
291 |
+
static $inline_tags_minus_br = '<a><abbr><acronym><b><big><button><cite><code><del><dfn><em><font><i><img><input><ins><kbd><label><q><s><samp><select><small><span><strike><strong><sub><sup><textarea><tt><u><var>';
|
292 |
+
$content_with_newlines = (strip_tags($content, $inline_tags_minus_br)) ? $content : nl2br($content);
|
293 |
+
|
294 |
+
// Check to see if we have any block-level html, extracted from fHTML to reduce dependencies
|
295 |
+
$inline_tags = $inline_tags_minus_br . '<br>';
|
296 |
+
$no_block_html = strip_tags($content, $inline_tags) == $content;
|
297 |
+
|
298 |
+
// This code ensures the output is properly encoded for display in (X)HTML, extracted from fHTML to reduce dependencies
|
299 |
+
$reg_exp = "/<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|&(?:#\d+|\w+);|<\!--.*?-->/";
|
300 |
+
preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
|
301 |
+
$text_matches = preg_split($reg_exp, $content_with_newlines);
|
302 |
+
|
303 |
+
foreach ($text_matches as $key => $value) {
|
304 |
+
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
305 |
+
}
|
306 |
+
|
307 |
+
for ($i = 0; $i < sizeof($html_matches); $i++) {
|
308 |
+
$text_matches[$i] .= $html_matches[$i][0];
|
309 |
+
}
|
310 |
+
|
311 |
+
$content_with_newlines = implode($text_matches);
|
312 |
+
|
313 |
+
$output = ($no_block_html) ? '<p>' : '';
|
314 |
+
$output .= $content_with_newlines;
|
315 |
+
$output .= ($no_block_html) ? '</p>' : '';
|
316 |
+
|
317 |
+
return $output;
|
318 |
+
}
|
319 |
+
|
320 |
+
/**
|
321 |
+
* Prints the message inside of a div with the class being 'exception %THIS_EXCEPTION_CLASS_NAME%'
|
322 |
+
*
|
323 |
+
* @return void
|
324 |
+
*/
|
325 |
+
public function printMessage() {
|
326 |
+
echo '<div class="exception ' . $this->getCSSClass() . '">';
|
327 |
+
echo $this->prepare($this->message);
|
328 |
+
echo '</div>';
|
329 |
+
}
|
330 |
+
|
331 |
+
/**
|
332 |
+
* Prints the backtrace to currently called exception inside of a pre tag with the class being 'exception %THIS_EXCEPTION_CLASS_NAME% trace'
|
333 |
+
*
|
334 |
+
* @return void
|
335 |
+
*/
|
336 |
+
public function printTrace() {
|
337 |
+
echo '<pre class="exception ' . $this->getCSSClass() . ' trace">';
|
338 |
+
echo $this->formatTrace();
|
339 |
+
echo '</pre>';
|
340 |
+
}
|
341 |
+
|
342 |
+
/**
|
343 |
+
* Reorders list items in the message based on simple string matching
|
344 |
+
*
|
345 |
+
* @param string $match This should be a string to match to one of the list items - whatever the order this is in the parameter list will be the order of the list item in the adjusted message
|
346 |
+
* @param string ...
|
347 |
+
* @return fException The exception object, to allow for method chaining
|
348 |
+
*/
|
349 |
+
public function reorderMessage($match) {
|
350 |
+
// If we can't find a list, don't bother continuing
|
351 |
+
if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $message_parts)) {
|
352 |
+
return $this;
|
353 |
+
}
|
354 |
+
|
355 |
+
$matching_array = func_get_args();
|
356 |
+
// This ensures that we match on the longest string first
|
357 |
+
uasort($matching_array, array('self', 'sortMatchingArray'));
|
358 |
+
|
359 |
+
$beginning = $message_parts[1];
|
360 |
+
$list_contents = $message_parts[2];
|
361 |
+
$ending = $message_parts[3];
|
362 |
+
|
363 |
+
preg_match_all('#<li(.*?)</li>#i', $list_contents, $list_items, PREG_SET_ORDER);
|
364 |
+
|
365 |
+
$ordered_items = array_fill(0, sizeof($matching_array), array());
|
366 |
+
$other_items = array();
|
367 |
+
|
368 |
+
foreach ($list_items as $list_item) {
|
369 |
+
foreach ($matching_array as $num => $match_string) {
|
370 |
+
if (strpos($list_item[1], $match_string) !== FALSE) {
|
371 |
+
$ordered_items[$num][] = $list_item[0];
|
372 |
+
continue 2;
|
373 |
+
}
|
374 |
+
}
|
375 |
+
|
376 |
+
$other_items[] = $list_item[0];
|
377 |
+
}
|
378 |
+
|
379 |
+
$final_list = array();
|
380 |
+
foreach ($ordered_items as $ordered_item) {
|
381 |
+
$final_list = array_merge($final_list, $ordered_item);
|
382 |
+
}
|
383 |
+
$final_list = array_merge($final_list, $other_items);
|
384 |
+
|
385 |
+
$this->message = $beginning . join("\n", $final_list) . $ending;
|
386 |
+
|
387 |
+
return $this;
|
388 |
+
}
|
389 |
+
|
390 |
+
/**
|
391 |
+
* Allows the message to be overwriten
|
392 |
+
*
|
393 |
+
* @param string $new_message The new message for the exception
|
394 |
+
* @return void
|
395 |
+
*/
|
396 |
+
public function setMessage($new_message) {
|
397 |
+
$this->message = $new_message;
|
398 |
+
}
|
399 |
+
|
400 |
+
/**
|
401 |
+
* Splits an exception with an HTML list into multiple strings each containing part of the original message
|
402 |
+
*
|
403 |
+
* This method should be called with two or more parameters of arrays of
|
404 |
+
* string to match. If any of the provided strings are matching in a list
|
405 |
+
* item in the exception message, a new copy of the message will be created
|
406 |
+
* containing just the matching list items.
|
407 |
+
*
|
408 |
+
* Here is an exception message to be split:
|
409 |
+
*
|
410 |
+
* {{{
|
411 |
+
* #!html
|
412 |
+
* <p>The following problems were found:</p>
|
413 |
+
* <ul>
|
414 |
+
* <li>First Name: Please enter a value</li>
|
415 |
+
* <li>Last Name: Please enter a value</li>
|
416 |
+
* <li>Email: Please enter a value</li>
|
417 |
+
* <li>Address: Please enter a value</li>
|
418 |
+
* <li>City: Please enter a value</li>
|
419 |
+
* <li>State: Please enter a value</li>
|
420 |
+
* <li>Zip Code: Please enter a value</li>
|
421 |
+
* </ul>
|
422 |
+
* }}}
|
423 |
+
*
|
424 |
+
* The following PHP would split the exception into two messages:
|
425 |
+
*
|
426 |
+
* {{{
|
427 |
+
* #!php
|
428 |
+
* list ($name_exception, $address_exception) = $exception->splitMessage(
|
429 |
+
* array('First Name', 'Last Name', 'Email'),
|
430 |
+
* array('Address', 'City', 'State', 'Zip Code')
|
431 |
+
* );
|
432 |
+
* }}}
|
433 |
+
*
|
434 |
+
* The resulting messages would be:
|
435 |
+
*
|
436 |
+
* {{{
|
437 |
+
* #!html
|
438 |
+
* <p>The following problems were found:</p>
|
439 |
+
* <ul>
|
440 |
+
* <li>First Name: Please enter a value</li>
|
441 |
+
* <li>Last Name: Please enter a value</li>
|
442 |
+
* <li>Email: Please enter a value</li>
|
443 |
+
* </ul>
|
444 |
+
* }}}
|
445 |
+
*
|
446 |
+
* and
|
447 |
+
*
|
448 |
+
* {{{
|
449 |
+
* #!html
|
450 |
+
* <p>The following problems were found:</p>
|
451 |
+
* <ul>
|
452 |
+
* <li>Address: Please enter a value</li>
|
453 |
+
* <li>City: Please enter a value</li>
|
454 |
+
* <li>State: Please enter a value</li>
|
455 |
+
* <li>Zip Code: Please enter a value</li>
|
456 |
+
* </ul>
|
457 |
+
* }}}
|
458 |
+
*
|
459 |
+
* If no list items match the strings in a parameter, the result will be
|
460 |
+
* an empty string, allowing for simple display:
|
461 |
+
*
|
462 |
+
* {{{
|
463 |
+
* #!php
|
464 |
+
* fHTML::show($name_exception, 'error');
|
465 |
+
* }}}
|
466 |
+
*
|
467 |
+
* An empty string is returned when none of the list items matched the
|
468 |
+
* strings in the parameter. If no list items are found, the first value in
|
469 |
+
* the returned array will be the existing message and all other array
|
470 |
+
* values will be an empty string.
|
471 |
+
*
|
472 |
+
* @param array $list_item_matches An array of strings to filter the list items by, list items will be ordered in the same order as this array
|
473 |
+
* @param array ...
|
474 |
+
* @return array This will contain an array of strings corresponding to the parameters passed - see method description for details
|
475 |
+
*/
|
476 |
+
public function splitMessage($list_item_matches) {
|
477 |
+
$class = get_class($this);
|
478 |
+
|
479 |
+
$matching_arrays = func_get_args();
|
480 |
+
|
481 |
+
if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $matches)) {
|
482 |
+
return array_merge(array($this->message), array_fill(0, sizeof($matching_arrays) - 1, ''));
|
483 |
+
}
|
484 |
+
|
485 |
+
$beginning_html = $matches[1];
|
486 |
+
$list_items_html = $matches[2];
|
487 |
+
$ending_html = $matches[3];
|
488 |
+
|
489 |
+
preg_match_all('#<li(.*?)</li>#i', $list_items_html, $list_items, PREG_SET_ORDER);
|
490 |
+
|
491 |
+
$output = array();
|
492 |
+
|
493 |
+
foreach ($matching_arrays as $matching_array) {
|
494 |
+
|
495 |
+
// This ensures that we match on the longest string first
|
496 |
+
uasort($matching_array, array('self', 'sortMatchingArray'));
|
497 |
+
|
498 |
+
// We may match more than one list item per matching string, so we need a multi-dimensional array to hold them
|
499 |
+
$matched_list_items = array_fill(0, sizeof($matching_array), array());
|
500 |
+
$found = FALSE;
|
501 |
+
|
502 |
+
foreach ($list_items as $list_item) {
|
503 |
+
foreach ($matching_array as $match_num => $matching_string) {
|
504 |
+
if (strpos($list_item[1], $matching_string) !== FALSE) {
|
505 |
+
$matched_list_items[$match_num][] = $list_item[0];
|
506 |
+
$found = TRUE;
|
507 |
+
continue 2;
|
508 |
+
}
|
509 |
+
}
|
510 |
+
}
|
511 |
+
|
512 |
+
if (!$found) {
|
513 |
+
$output[] = '';
|
514 |
+
continue;
|
515 |
+
}
|
516 |
+
|
517 |
+
// This merges all of the multi-dimensional arrays back to one so we can do a simple join
|
518 |
+
$merged_list_items = array();
|
519 |
+
foreach ($matched_list_items as $match_num => $matched_items) {
|
520 |
+
$merged_list_items = array_merge($merged_list_items, $matched_items);
|
521 |
+
}
|
522 |
+
|
523 |
+
$output[] = $beginning_html . join("\n", $merged_list_items) . $ending_html;
|
524 |
+
}
|
525 |
+
|
526 |
+
return $output;
|
527 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
|
529 |
+
}
|
530 |
|
531 |
/**
|
532 |
* Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
|
lib/fUnexpectedException.php
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<?php
|
|
|
2 |
/**
|
3 |
* An exception that should probably not be handled by the display code, fCore::enableExceptionHandler() is recommended
|
4 |
*
|
@@ -13,24 +14,22 @@
|
|
13 |
* @changes 1.0.0b2 Updated ::printMessage() to use an ASCII dash to prevent encoding issues when an output encoding is not specified [wb, 2011-05-09]
|
14 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
15 |
*/
|
16 |
-
class fUnexpectedException extends fException
|
17 |
-
{
|
18 |
-
/**
|
19 |
-
* Prints out a generic error message inside of a `div` with the class being `'exception {exception_class_name}'`
|
20 |
-
*
|
21 |
-
* @return void
|
22 |
-
*/
|
23 |
-
public function printMessage()
|
24 |
-
{
|
25 |
-
echo '<div class="exception ' . $this->getCSSClass() . '"><p>';
|
26 |
-
echo self::compose(
|
27 |
-
'It appears an error has occurred - we apologize for the inconvenience. The problem may be resolved if you try again.'
|
28 |
-
);
|
29 |
-
echo '</p></div>';
|
30 |
-
}
|
31 |
-
}
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
|
|
34 |
|
35 |
/**
|
36 |
* Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
|
1 |
<?php
|
2 |
+
|
3 |
/**
|
4 |
* An exception that should probably not be handled by the display code, fCore::enableExceptionHandler() is recommended
|
5 |
*
|
14 |
* @changes 1.0.0b2 Updated ::printMessage() to use an ASCII dash to prevent encoding issues when an output encoding is not specified [wb, 2011-05-09]
|
15 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
16 |
*/
|
17 |
+
class fUnexpectedException extends fException {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
+
/**
|
20 |
+
* Prints out a generic error message inside of a `div` with the class being `'exception {exception_class_name}'`
|
21 |
+
*
|
22 |
+
* @return void
|
23 |
+
*/
|
24 |
+
public function printMessage() {
|
25 |
+
echo '<div class="exception ' . $this->getCSSClass() . '"><p>';
|
26 |
+
echo self::compose(
|
27 |
+
'It appears an error has occurred - we apologize for the inconvenience. The problem may be resolved if you try again.'
|
28 |
+
);
|
29 |
+
echo '</p></div>';
|
30 |
+
}
|
31 |
|
32 |
+
}
|
33 |
|
34 |
/**
|
35 |
* Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
|
lib/fValidationException.php
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<?php
|
|
|
2 |
/**
|
3 |
* An exception caused by a data not matching a rule or set of rules
|
4 |
*
|
@@ -16,177 +17,156 @@
|
|
16 |
* @changes 1.0.0b2 Added a custom ::__construct() to handle arrays of messages [wb, 2009-09-17]
|
17 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
18 |
*/
|
19 |
-
class fValidationException extends fExpectedException
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
*
|
168 |
-
* @param array $errors An array of (possibly nested) child record errors
|
169 |
-
* @return array An array of string error messages
|
170 |
-
*/
|
171 |
-
private function formatErrorArray($errors)
|
172 |
-
{
|
173 |
-
$new_errors = array();
|
174 |
-
foreach ($errors as $error) {
|
175 |
-
if (!is_array($error)) {
|
176 |
-
$new_errors[] = $error;
|
177 |
-
} else {
|
178 |
-
$new_errors[] = sprintf(
|
179 |
-
"<span>%1\$s</span>\n<ul>\n<li>%2\$s</li>\n</ul>",
|
180 |
-
$error['name'],
|
181 |
-
join("</li>\n<li>", $this->formatErrorArray($error['errors']))
|
182 |
-
);
|
183 |
-
}
|
184 |
-
}
|
185 |
-
return $new_errors;
|
186 |
-
}
|
187 |
-
}
|
188 |
-
|
189 |
|
|
|
190 |
|
191 |
/**
|
192 |
* Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
|
1 |
<?php
|
2 |
+
|
3 |
/**
|
4 |
* An exception caused by a data not matching a rule or set of rules
|
5 |
*
|
17 |
* @changes 1.0.0b2 Added a custom ::__construct() to handle arrays of messages [wb, 2009-09-17]
|
18 |
* @changes 1.0.0b The initial implementation [wb, 2007-06-14]
|
19 |
*/
|
20 |
+
class fValidationException extends fExpectedException {
|
21 |
+
|
22 |
+
const formatField = 'fValidationException::formatField';
|
23 |
+
const removeFieldNames = 'fValidationException::removeFieldNames';
|
24 |
+
const setFieldFormat = 'fValidationException::setFieldFormat';
|
25 |
+
|
26 |
+
/**
|
27 |
+
* The formatting string to use for field names
|
28 |
+
*
|
29 |
+
* @var string
|
30 |
+
*/
|
31 |
+
static protected $field_format = '%s: ';
|
32 |
+
|
33 |
+
/**
|
34 |
+
* Accepts a field name and formats it based on the formatting string set via ::setFieldFormat()
|
35 |
+
*
|
36 |
+
* @param string $field The name of the field to format
|
37 |
+
* @return string The formatted field name
|
38 |
+
*/
|
39 |
+
static public function formatField($field) {
|
40 |
+
return sprintf(self::$field_format, $field);
|
41 |
+
}
|
42 |
+
|
43 |
+
/**
|
44 |
+
* Removes the field names from normal validation messages, leaving just the message part
|
45 |
+
*
|
46 |
+
* @param array $messages The messages to remove the field names from
|
47 |
+
* @return array The messages without field names
|
48 |
+
*/
|
49 |
+
static public function removeFieldNames($messages) {
|
50 |
+
$token_field = self::formatField('__TOKEN__');
|
51 |
+
$replace_regex = '#^' . str_replace('__TOKEN__', '(.*?)', preg_quote($token_field, '#')) . '#';
|
52 |
+
|
53 |
+
$output = array();
|
54 |
+
foreach ($messages as $column => $message) {
|
55 |
+
if (is_array($message)) {
|
56 |
+
$message['errors'] = self::removeFieldNames($message['errors']);
|
57 |
+
$output[$column] = $message;
|
58 |
+
} else {
|
59 |
+
$output[$column] = preg_replace($replace_regex, '', $message);
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
return $output;
|
64 |
+
}
|
65 |
+
|
66 |
+
/**
|
67 |
+
* Set the format to be applied to all field names used in fValidationExceptions
|
68 |
+
*
|
69 |
+
* The format should contain exactly one `%s`
|
70 |
+
* [http://php.net/sprintf sprintf()] conversion specification, which will
|
71 |
+
* be replaced with the field name. Any literal `%` characters should be
|
72 |
+
* written as `%%`.
|
73 |
+
*
|
74 |
+
* The default format is just `%s: `, which simply inserts a `:` and space
|
75 |
+
* after the field name.
|
76 |
+
*
|
77 |
+
* @param string $format A string to format the field name with - `%s` will be replaced with the field name
|
78 |
+
* @return void
|
79 |
+
*/
|
80 |
+
static public function setFieldFormat($format) {
|
81 |
+
if (substr_count(str_replace('%%', '', $format), '%') != 1 || strpos($format, '%s') === FALSE) {
|
82 |
+
throw new fProgrammerException(
|
83 |
+
'The format, %s, has more or less than exactly one %%s sprintf() conversion specification', $format
|
84 |
+
);
|
85 |
+
}
|
86 |
+
self::$field_format = $format;
|
87 |
+
}
|
88 |
+
|
89 |
+
/**
|
90 |
+
* Sets the message for the exception, allowing for custom formatting beyond fException
|
91 |
+
*
|
92 |
+
* If this method receives exactly two parameters, a string and an array,
|
93 |
+
* the string will be used as a message in a HTML `<p>` tag and the array
|
94 |
+
* will be turned into an unorder list `<ul>` tag with each element in the
|
95 |
+
* array being an `<li>` tag. It is possible to pass an optional exception
|
96 |
+
* code as a third parameter.
|
97 |
+
*
|
98 |
+
* The following PHP:
|
99 |
+
*
|
100 |
+
* {{{
|
101 |
+
* #!php
|
102 |
+
* throw new fValidationException(
|
103 |
+
* 'The following problems were found:',
|
104 |
+
* array(
|
105 |
+
* 'Please provide your name',
|
106 |
+
* 'Please provide your email address'
|
107 |
+
* )
|
108 |
+
* );
|
109 |
+
* }}}
|
110 |
+
*
|
111 |
+
* Would create the message:
|
112 |
+
*
|
113 |
+
* {{{
|
114 |
+
* #!text/html
|
115 |
+
* <p>The following problems were found:</p>
|
116 |
+
* <ul>
|
117 |
+
* <li>Please provide your name</li>
|
118 |
+
* <li>Please provide your email address</li>
|
119 |
+
* </ul>
|
120 |
+
* }}}
|
121 |
+
*
|
122 |
+
* If the parameters are anything else, they will be passed to
|
123 |
+
* fException::__construct().
|
124 |
+
*
|
125 |
+
* @param string $message The beginning message for the exception. This will be placed in a `<p>` tag.
|
126 |
+
* @param array $sub_messages An array of strings to place in a `<ul>` tag
|
127 |
+
* @param mixed $code The optional exception code
|
128 |
+
* @return fException
|
129 |
+
*/
|
130 |
+
public function __construct($message = '') {
|
131 |
+
$params = func_get_args();
|
132 |
+
|
133 |
+
if ((count($params) == 2 || count($params) == 3) && is_string($params[0]) && is_array($params[1])) {
|
134 |
+
|
135 |
+
|
136 |
+
$message = sprintf("<p>%1\$s</p>\n<ul>\n<li>%2\$s</li>\n</ul>", self::compose($params[0]), join("</li>\n<li>", $this->formatErrorArray($params[1])));
|
137 |
+
|
138 |
+
$params = array_merge(
|
139 |
+
// This escapes % signs since fException is going to look for sprintf formatting codes
|
140 |
+
array(str_replace('%', '%%', $message)),
|
141 |
+
// This grabs the exception code if one is defined
|
142 |
+
array_slice($params, 2)
|
143 |
+
);
|
144 |
+
}
|
145 |
+
|
146 |
+
call_user_func_array(
|
147 |
+
array($this, 'fException::__construct'), $params
|
148 |
+
);
|
149 |
+
}
|
150 |
+
|
151 |
+
/**
|
152 |
+
* Takes an error array that may or may not be nested and returns a HTML string representation
|
153 |
+
*
|
154 |
+
* @param array $errors An array of (possibly nested) child record errors
|
155 |
+
* @return array An array of string error messages
|
156 |
+
*/
|
157 |
+
private function formatErrorArray($errors) {
|
158 |
+
$new_errors = array();
|
159 |
+
foreach ($errors as $error) {
|
160 |
+
if (!is_array($error)) {
|
161 |
+
$new_errors[] = $error;
|
162 |
+
} else {
|
163 |
+
$new_errors[] = sprintf("<span>%1\$s</span>\n<ul>\n<li>%2\$s</li>\n</ul>", $error['name'], join("</li>\n<li>", $this->formatErrorArray($error['errors'])));
|
164 |
+
}
|
165 |
+
}
|
166 |
+
return $new_errors;
|
167 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
|
169 |
+
}
|
170 |
|
171 |
/**
|
172 |
* Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
|
lib/pImapMailServer.php
CHANGED
@@ -140,7 +140,6 @@ class pImapMailServer extends pMailServer {
|
|
140 |
|
141 |
$output = array();
|
142 |
$mline = '';
|
143 |
-
//$response = $this->connection->write('FETCH ' . $start . ':' . $end . ' (UID INTERNALDATE RFC822.SIZE ENVELOPE)');
|
144 |
$response = $this->connection->write('FETCH ' . $start . ':' . $end . ' (UID INTERNALDATE RFC822.SIZE)');
|
145 |
foreach ($response as $line) {
|
146 |
DebugEcho("listMessages: line: '$line'");
|
@@ -154,28 +153,6 @@ class pImapMailServer extends pMailServer {
|
|
154 |
$info['received'] = $this->cleanDate($details['internaldate']);
|
155 |
$info['size'] = $details['rfc822.size'];
|
156 |
|
157 |
-
// $envelope = $details['envelope'];
|
158 |
-
// $info['date'] = $envelope[0] != 'NIL' ? $envelope[0] : '';
|
159 |
-
// $info['from'] = $this->joinEmails($envelope[2]);
|
160 |
-
// if (preg_match('#=\?[^\?]+\?[QB]\?[^\?]+\?=#', $envelope[1])) {
|
161 |
-
// do {
|
162 |
-
// $last_subject = $envelope[1];
|
163 |
-
// $envelope[1] = preg_replace('#(=\?([^\?]+)\?[QB]\?[^\?]+\?=) (\s*=\?\2)#', '\1\3', $envelope[1]);
|
164 |
-
// } while ($envelope[1] != $last_subject);
|
165 |
-
// $info['subject'] = $this->decodeHeader($envelope[1]);
|
166 |
-
// } else {
|
167 |
-
// $info['subject'] = $envelope[1] == 'NIL' ? '' : $this->decodeHeader($envelope[1]);
|
168 |
-
// }
|
169 |
-
// if ($envelope[9] != 'NIL') {
|
170 |
-
// $info['message_id'] = $envelope[9];
|
171 |
-
// }
|
172 |
-
// if ($envelope[5] != 'NIL') {
|
173 |
-
// $info['to'] = $this->joinEmails($envelope[5]);
|
174 |
-
// }
|
175 |
-
// if ($envelope[8] != 'NIL') {
|
176 |
-
// $info['in_reply_to'] = $envelope[8];
|
177 |
-
// }
|
178 |
-
|
179 |
$output[$info['uid']] = $info;
|
180 |
$mline = '';
|
181 |
}
|
140 |
|
141 |
$output = array();
|
142 |
$mline = '';
|
|
|
143 |
$response = $this->connection->write('FETCH ' . $start . ':' . $end . ' (UID INTERNALDATE RFC822.SIZE)');
|
144 |
foreach ($response as $line) {
|
145 |
DebugEcho("listMessages: line: '$line'");
|
153 |
$info['received'] = $this->cleanDate($details['internaldate']);
|
154 |
$info['size'] = $details['rfc822.size'];
|
155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
$output[$info['uid']] = $info;
|
157 |
$mline = '';
|
158 |
}
|
postie.php
CHANGED
@@ -4,7 +4,7 @@
|
|
4 |
Plugin Name: Postie
|
5 |
Plugin URI: http://PostiePlugin.com/
|
6 |
Description: Create posts via email. Significantly upgrades the Post by Email features of WordPress.
|
7 |
-
Version: 1.8.
|
8 |
Author: Wayne Allen
|
9 |
Author URI: http://PostiePlugin.com/
|
10 |
License: GPL2
|
@@ -28,7 +28,7 @@
|
|
28 |
*/
|
29 |
|
30 |
/*
|
31 |
-
$Id: postie.php
|
32 |
*/
|
33 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib/fException.php");
|
34 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib/fUnexpectedException.php");
|
@@ -49,7 +49,7 @@ require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib/pPop3MailServer.php"
|
|
49 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib_autolink.php");
|
50 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "postie-functions.php");
|
51 |
|
52 |
-
define('POSTIE_VERSION', '1.8.
|
53 |
define("POSTIE_ROOT", dirname(__FILE__));
|
54 |
define("POSTIE_URL", WP_PLUGIN_URL . '/' . basename(dirname(__FILE__)));
|
55 |
|
4 |
Plugin Name: Postie
|
5 |
Plugin URI: http://PostiePlugin.com/
|
6 |
Description: Create posts via email. Significantly upgrades the Post by Email features of WordPress.
|
7 |
+
Version: 1.8.14
|
8 |
Author: Wayne Allen
|
9 |
Author URI: http://PostiePlugin.com/
|
10 |
License: GPL2
|
28 |
*/
|
29 |
|
30 |
/*
|
31 |
+
$Id: postie.php 1526284 2016-11-01 21:17:11Z WayneAllen $
|
32 |
*/
|
33 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib/fException.php");
|
34 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib/fUnexpectedException.php");
|
49 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "lib_autolink.php");
|
50 |
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . "postie-functions.php");
|
51 |
|
52 |
+
define('POSTIE_VERSION', '1.8.14');
|
53 |
define("POSTIE_ROOT", dirname(__FILE__));
|
54 |
define("POSTIE_URL", WP_PLUGIN_URL . '/' . basename(dirname(__FILE__)));
|
55 |
|
readme.txt
CHANGED
@@ -6,7 +6,7 @@ Plugin URI: http://PostiePlugin.com/
|
|
6 |
Tags: e-mail, email, post-by-email
|
7 |
Requires at least: 3.3.0
|
8 |
Tested up to: 4.6.1
|
9 |
-
Stable tag: 1.8.
|
10 |
License: GPLv2 or later
|
11 |
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
12 |
|
@@ -240,6 +240,9 @@ All script, style and body tags are stripped from html emails.
|
|
240 |
Attachments are now processed in the order they were attached.
|
241 |
|
242 |
== CHANGELOG ==
|
|
|
|
|
|
|
243 |
= 1.8.13 (2016-10-31) =
|
244 |
* Fix bug where inline images were not being found due to case differences, e.g. img_4180.jpg vs. IMG_4180.JPG
|
245 |
|
6 |
Tags: e-mail, email, post-by-email
|
7 |
Requires at least: 3.3.0
|
8 |
Tested up to: 4.6.1
|
9 |
+
Stable tag: 1.8.14
|
10 |
License: GPLv2 or later
|
11 |
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
12 |
|
240 |
Attachments are now processed in the order they were attached.
|
241 |
|
242 |
== CHANGELOG ==
|
243 |
+
= 1.8.14 (2016-11-01) =
|
244 |
+
* Fix bug where OS detection failure was preventing email processing
|
245 |
+
|
246 |
= 1.8.13 (2016-10-31) =
|
247 |
* Fix bug where inline images were not being found due to case differences, e.g. img_4180.jpg vs. IMG_4180.JPG
|
248 |
|