Postie - Version 1.8.14

Version Description

(2016-11-01) = * Fix bug where OS detection failure was preventing email processing

Download this release

Release Info

Developer WayneAllen
Plugin Icon 128x128 Postie
Version 1.8.14
Comparing to
See all releases

Code changes from version 1.8.13 to 1.8.14

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.13
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('Unable to determine the current OS');
 
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
- // The following constants allow for nice looking callbacks to static methods
56
- const combineNameEmail = 'fEmail::combineNameEmail';
57
- const fixQmail = 'fEmail::fixQmail';
58
- const getFQDN = 'fEmail::getFQDN';
59
- const reset = 'fEmail::reset';
60
- const unindentExpand = 'fEmail::unindentExpand';
61
-
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,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
- * 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,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
- * Callbacks for when exceptions are created
26
- *
27
- * @var array
28
- */
29
- static private $callbacks = array();
30
-
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
- {
42
- $components = array_slice(func_get_args(), 1);
43
-
44
- // Handles components passed as an array
45
- if (sizeof($components) == 1 && is_array($components[0])) {
46
- $components = $components[0];
47
- }
48
-
49
- // If fText is loaded, use it
50
- if (class_exists('fText', FALSE)) {
51
- return call_user_func_array(
52
- array('fText', 'compose'),
53
- array($message, $components)
54
- );
55
-
56
- } else {
57
- return vsprintf($message, $components);
58
- }
59
- }
60
-
61
-
62
- /**
63
- * Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
64
- *
65
- * The string output format of this method is very similar to the output of
66
- * [http://php.net/print_r print_r()] except that the following values
67
- * are represented as special strings:
68
- *
69
- * - `TRUE`: `'{true}'`
70
- * - `FALSE`: `'{false}'`
71
- * - `NULL`: `'{null}'`
72
- * - `''`: `'{empty_string}'`
73
- *
74
- * @param mixed $data The value to dump
75
- * @return string The string representation of the value
76
- */
77
- static protected function dump($data)
78
- {
79
- if (is_bool($data)) {
80
- return ($data) ? '{true}' : '{false}';
81
-
82
- } elseif (is_null($data)) {
83
- return '{null}';
84
-
85
- } elseif ($data === '') {
86
- return '{empty_string}';
87
-
88
- } elseif (is_array($data) || is_object($data)) {
89
-
90
- ob_start();
91
- var_dump($data);
92
- $output = ob_get_contents();
93
- ob_end_clean();
94
-
95
- // Make the var dump more like a print_r
96
- $output = preg_replace('#=>\n( )+(?=[a-zA-Z]|&)#m', ' => ', $output);
97
- $output = str_replace('string(0) ""', '{empty_string}', $output);
98
- $output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
99
- $output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
100
- $output = preg_replace('#string\(\d+\) "#', '', $output);
101
- $output = preg_replace('#"(\n( )*)(?=\[|\})#', '\1', $output);
102
- $output = preg_replace('#(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
103
- $output = preg_replace('#((?: )+)\["(.*?)"\]#', '\1[\2]', $output);
104
- $output = preg_replace('#(?:&)?array\(\d+\) \{\n((?: )*)((?: )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
105
- $output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?: )*)((?: )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
106
- $output = preg_replace('#^((?: )+)}(?=\n|$)#m', "\\1)\n", $output);
107
- $output = substr($output, 0, -2) . ')';
108
-
109
- // Fix indenting issues with the var dump output
110
- $output_lines = explode("\n", $output);
111
- $new_output = array();
112
- $stack = 0;
113
- foreach ($output_lines as $line) {
114
- if (preg_match('#^((?: )*)([^ ])#', $line, $match)) {
115
- $spaces = strlen($match[1]);
116
- if ($spaces && $match[2] == '(') {
117
- $stack += 1;
118
- }
119
- $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
120
- if ($spaces && $match[2] == ')') {
121
- $stack -= 1;
122
- }
123
- } else {
124
- $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
125
- }
126
- }
127
-
128
- return join("\n", $new_output);
129
-
130
- } else {
131
- return (string) $data;
132
- }
133
- }
134
-
135
-
136
- /**
137
- * Adds a callback for when certain types of exceptions are created
138
- *
139
- * The callback will be called when any exception of this class, or any
140
- * child class, specified is tossed. A single parameter will be passed
141
- * to the callback, which will be the exception object.
142
- *
143
- * @param callback $callback The callback
144
- * @param string $exception_type The type of exception to call the callback for
145
- * @return void
146
- */
147
- static public function registerCallback($callback, $exception_type=NULL)
148
- {
149
- if ($exception_type === NULL) {
150
- $exception_type = 'fException';
151
- }
152
-
153
- if (!isset(self::$callbacks[$exception_type])) {
154
- self::$callbacks[$exception_type] = array();
155
- }
156
-
157
- if (is_string($callback) && strpos($callback, '::') !== FALSE) {
158
- $callback = explode('::', $callback);
159
- }
160
-
161
- self::$callbacks[$exception_type][] = $callback;
162
- }
163
-
164
-
165
- /**
166
- * Compares the message matching strings by longest first so that the longest matches are made first
167
- *
168
- * @param string $a The first string to compare
169
- * @param string $b The second string to compare
170
- * @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
171
- */
172
- static private function sortMatchingArray($a, $b)
173
- {
174
- return -1 * strnatcmp(strlen($a), strlen($b));
175
- }
176
-
177
-
178
- /**
179
- * Sets the message for the exception, allowing for string interpolation and internationalization
180
- *
181
- * The `$message` can contain any number of formatting placeholders for
182
- * string and number interpolation via [http://php.net/sprintf `sprintf()`].
183
- * Any `%` signs that do not appear to be part of a valid formatting
184
- * placeholder will be automatically escaped with a second `%`.
185
- *
186
- * The following aspects of valid `sprintf()` formatting codes are not
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
- $message,
212
- $matches
213
- );
214
-
215
- // Handle %s that weren't properly escaped
216
- $formats = $matches[1];
217
- $delimeters = ($formats) ? array_fill(0, sizeof($formats), '#') : array();
218
- $lookahead = join(
219
- '|',
220
- array_map(
221
- 'preg_quote',
222
- $formats,
223
- $delimeters
224
- )
225
- );
226
- $lookahead = ($lookahead) ? '|' . $lookahead : '';
227
- $message = preg_replace('#(?<!%)%(?!%' . $lookahead . ')#', '%%', $message);
228
-
229
- // If we have an extra argument, it is the exception code
230
- $code = NULL;
231
- if ($required_args == sizeof($args) - 1) {
232
- $code = array_pop($args);
233
- }
234
-
235
- if (sizeof($args) != $required_args) {
236
- $message = self::compose(
237
- '%1$d components were passed to the %2$s constructor, while %3$d were specified in the message',
238
- sizeof($args),
239
- get_class($this),
240
- $required_args
241
- );
242
- throw new Exception($message);
243
- }
244
-
245
- $args = array_map(array('fException', 'dump'), $args);
246
-
247
- parent::__construct(self::compose($message, $args));
248
- $this->code = $code;
249
-
250
- foreach (self::$callbacks as $class => $callbacks) {
251
- foreach ($callbacks as $callback) {
252
- if ($this instanceof $class) {
253
- call_user_func($callback, $this);
254
- }
255
- }
256
- }
257
- }
258
-
259
-
260
- /**
261
- * All requests that hit this method should be requests for callbacks
262
- *
263
- * @internal
264
- *
265
- * @param string $method The method to create a callback for
266
- * @return callback The callback for the method requested
267
- */
268
- public function __get($method)
269
- {
270
- return array($this, $method);
271
- }
272
-
273
-
274
- /**
275
- * Gets the backtrace to currently called exception
276
- *
277
- * @return string A nicely formatted backtrace to this exception
278
- */
279
- public function formatTrace()
280
- {
281
- $doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
282
- $doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
283
-
284
- $backtrace = explode("\n", $this->getTraceAsString());
285
- array_unshift($backtrace, $this->file . '(' . $this->line . ')');
286
- $backtrace = preg_replace('/^#\d+\s+/', '', $backtrace);
287
- $backtrace = str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $backtrace);
288
- $backtrace = array_diff($backtrace, array('{main}'));
289
- $backtrace = array_reverse($backtrace);
290
-
291
- return join("\n", $backtrace);
292
- }
293
-
294
-
295
- /**
296
- * Returns the CSS class name for printing information about the exception
297
- *
298
- * @return void
299
- */
300
- protected function getCSSClass()
301
- {
302
- $string = preg_replace('#^f#', '', get_class($this));
303
-
304
- do {
305
- $old_string = $string;
306
- $string = preg_replace('/([a-zA-Z])([0-9])/', '\1_\2', $string);
307
- $string = preg_replace('/([a-z0-9A-Z])([A-Z])/', '\1_\2', $string);
308
- } while ($old_string != $string);
309
-
310
- return strtolower($string);
311
- }
312
-
313
-
314
- /**
315
- * Prepares content for output into HTML
316
- *
317
- * @return string The prepared content
318
- */
319
- protected function prepare($content)
320
- {
321
- // See if the message has newline characters but not br tags, extracted from fHTML to reduce dependencies
322
- 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>';
323
- $content_with_newlines = (strip_tags($content, $inline_tags_minus_br)) ? $content : nl2br($content);
324
-
325
- // Check to see if we have any block-level html, extracted from fHTML to reduce dependencies
326
- $inline_tags = $inline_tags_minus_br . '<br>';
327
- $no_block_html = strip_tags($content, $inline_tags) == $content;
328
-
329
- // This code ensures the output is properly encoded for display in (X)HTML, extracted from fHTML to reduce dependencies
330
- $reg_exp = "/<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|&(?:#\d+|\w+);|<\!--.*?-->/";
331
- preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
332
- $text_matches = preg_split($reg_exp, $content_with_newlines);
333
-
334
- foreach($text_matches as $key => $value) {
335
- $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
336
- }
337
-
338
- for ($i = 0; $i < sizeof($html_matches); $i++) {
339
- $text_matches[$i] .= $html_matches[$i][0];
340
- }
341
-
342
- $content_with_newlines = implode($text_matches);
343
-
344
- $output = ($no_block_html) ? '<p>' : '';
345
- $output .= $content_with_newlines;
346
- $output .= ($no_block_html) ? '</p>' : '';
347
-
348
- return $output;
349
- }
350
-
351
-
352
- /**
353
- * Prints the message inside of a div with the class being 'exception %THIS_EXCEPTION_CLASS_NAME%'
354
- *
355
- * @return void
356
- */
357
- public function printMessage()
358
- {
359
- echo '<div class="exception ' . $this->getCSSClass() . '">';
360
- echo $this->prepare($this->message);
361
- echo '</div>';
362
- }
363
-
364
-
365
- /**
366
- * Prints the backtrace to currently called exception inside of a pre tag with the class being 'exception %THIS_EXCEPTION_CLASS_NAME% trace'
367
- *
368
- * @return void
369
- */
370
- public function printTrace()
371
- {
372
- echo '<pre class="exception ' . $this->getCSSClass() . ' trace">';
373
- echo $this->formatTrace();
374
- echo '</pre>';
375
- }
376
-
377
-
378
- /**
379
- * Reorders list items in the message based on simple string matching
380
- *
381
- * @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
382
- * @param string ...
383
- * @return fException The exception object, to allow for method chaining
384
- */
385
- public function reorderMessage($match)
386
- {
387
- // If we can't find a list, don't bother continuing
388
- if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $message_parts)) {
389
- return $this;
390
- }
391
-
392
- $matching_array = func_get_args();
393
- // This ensures that we match on the longest string first
394
- uasort($matching_array, array('self', 'sortMatchingArray'));
395
-
396
- $beginning = $message_parts[1];
397
- $list_contents = $message_parts[2];
398
- $ending = $message_parts[3];
399
-
400
- preg_match_all('#<li(.*?)</li>#i', $list_contents, $list_items, PREG_SET_ORDER);
401
-
402
- $ordered_items = array_fill(0, sizeof($matching_array), array());
403
- $other_items = array();
404
-
405
- foreach ($list_items as $list_item) {
406
- foreach ($matching_array as $num => $match_string) {
407
- if (strpos($list_item[1], $match_string) !== FALSE) {
408
- $ordered_items[$num][] = $list_item[0];
409
- continue 2;
410
- }
411
- }
412
-
413
- $other_items[] = $list_item[0];
414
- }
415
-
416
- $final_list = array();
417
- foreach ($ordered_items as $ordered_item) {
418
- $final_list = array_merge($final_list, $ordered_item);
419
- }
420
- $final_list = array_merge($final_list, $other_items);
421
-
422
- $this->message = $beginning . join("\n", $final_list) . $ending;
423
-
424
- return $this;
425
- }
426
-
427
-
428
- /**
429
- * Allows the message to be overwriten
430
- *
431
- * @param string $new_message The new message for the exception
432
- * @return void
433
- */
434
- public function setMessage($new_message)
435
- {
436
- $this->message = $new_message;
437
- }
438
-
439
-
440
- /**
441
- * Splits an exception with an HTML list into multiple strings each containing part of the original message
442
- *
443
- * This method should be called with two or more parameters of arrays of
444
- * string to match. If any of the provided strings are matching in a list
445
- * item in the exception message, a new copy of the message will be created
446
- * containing just the matching list items.
447
- *
448
- * Here is an exception message to be split:
449
- *
450
- * {{{
451
- * #!html
452
- * <p>The following problems were found:</p>
453
- * <ul>
454
- * <li>First Name: Please enter a value</li>
455
- * <li>Last Name: Please enter a value</li>
456
- * <li>Email: Please enter a value</li>
457
- * <li>Address: Please enter a value</li>
458
- * <li>City: Please enter a value</li>
459
- * <li>State: Please enter a value</li>
460
- * <li>Zip Code: Please enter a value</li>
461
- * </ul>
462
- * }}}
463
- *
464
- * The following PHP would split the exception into two messages:
465
- *
466
- * {{{
467
- * #!php
468
- * list ($name_exception, $address_exception) = $exception->splitMessage(
469
- * array('First Name', 'Last Name', 'Email'),
470
- * array('Address', 'City', 'State', 'Zip Code')
471
- * );
472
- * }}}
473
- *
474
- * The resulting messages would be:
475
- *
476
- * {{{
477
- * #!html
478
- * <p>The following problems were found:</p>
479
- * <ul>
480
- * <li>First Name: Please enter a value</li>
481
- * <li>Last Name: Please enter a value</li>
482
- * <li>Email: Please enter a value</li>
483
- * </ul>
484
- * }}}
485
- *
486
- * and
487
- *
488
- * {{{
489
- * #!html
490
- * <p>The following problems were found:</p>
491
- * <ul>
492
- * <li>Address: Please enter a value</li>
493
- * <li>City: Please enter a value</li>
494
- * <li>State: Please enter a value</li>
495
- * <li>Zip Code: Please enter a value</li>
496
- * </ul>
497
- * }}}
498
- *
499
- * If no list items match the strings in a parameter, the result will be
500
- * an empty string, allowing for simple display:
501
- *
502
- * {{{
503
- * #!php
504
- * fHTML::show($name_exception, 'error');
505
- * }}}
506
- *
507
- * An empty string is returned when none of the list items matched the
508
- * strings in the parameter. If no list items are found, the first value in
509
- * the returned array will be the existing message and all other array
510
- * values will be an empty string.
511
- *
512
- * @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
513
- * @param array ...
514
- * @return array This will contain an array of strings corresponding to the parameters passed - see method description for details
515
- */
516
- public function splitMessage($list_item_matches)
517
- {
518
- $class = get_class($this);
519
-
520
- $matching_arrays = func_get_args();
521
-
522
- if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $matches)) {
523
- return array_merge(array($this->message), array_fill(0, sizeof($matching_arrays)-1, ''));
524
- }
525
-
526
- $beginning_html = $matches[1];
527
- $list_items_html = $matches[2];
528
- $ending_html = $matches[3];
529
-
530
- preg_match_all('#<li(.*?)</li>#i', $list_items_html, $list_items, PREG_SET_ORDER);
531
-
532
- $output = array();
533
-
534
- foreach ($matching_arrays as $matching_array) {
535
-
536
- // This ensures that we match on the longest string first
537
- uasort($matching_array, array('self', 'sortMatchingArray'));
538
-
539
- // We may match more than one list item per matching string, so we need a multi-dimensional array to hold them
540
- $matched_list_items = array_fill(0, sizeof($matching_array), array());
541
- $found = FALSE;
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
- const formatField = 'fValidationException::formatField';
22
- const removeFieldNames = 'fValidationException::removeFieldNames';
23
- const setFieldFormat = 'fValidationException::setFieldFormat';
24
-
25
- /**
26
- * The formatting string to use for field names
27
- *
28
- * @var string
29
- */
30
- static protected $field_format = '%s: ';
31
-
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
- {
41
- return sprintf(self::$field_format, $field);
42
- }
43
-
44
-
45
- /**
46
- * Removes the field names from normal validation messages, leaving just the message part
47
- *
48
- * @param array $messages The messages to remove the field names from
49
- * @return array The messages without field names
50
- */
51
- static public function removeFieldNames($messages)
52
- {
53
- $token_field = self::formatField('__TOKEN__');
54
- $replace_regex = '#^' . str_replace('__TOKEN__', '(.*?)', preg_quote($token_field, '#')) . '#';
55
-
56
- $output = array();
57
- foreach ($messages as $column => $message) {
58
- if (is_array($message)) {
59
- $message['errors'] = self::removeFieldNames($message['errors']);
60
- $output[$column] = $message;
61
- } else {
62
- $output[$column] = preg_replace($replace_regex, '', $message);
63
- }
64
- }
65
-
66
- return $output;
67
- }
68
-
69
-
70
- /**
71
- * Set the format to be applied to all field names used in fValidationExceptions
72
- *
73
- * The format should contain exactly one `%s`
74
- * [http://php.net/sprintf sprintf()] conversion specification, which will
75
- * be replaced with the field name. Any literal `%` characters should be
76
- * written as `%%`.
77
- *
78
- * The default format is just `%s: `, which simply inserts a `:` and space
79
- * after the field name.
80
- *
81
- * @param string $format A string to format the field name with - `%s` will be replaced with the field name
82
- * @return void
83
- */
84
- static public function setFieldFormat($format)
85
- {
86
- if (substr_count(str_replace('%%', '', $format), '%') != 1 || strpos($format, '%s') === FALSE) {
87
- throw new fProgrammerException(
88
- 'The format, %s, has more or less than exactly one %%s sprintf() conversion specification',
89
- $format
90
- );
91
- }
92
- self::$field_format = $format;
93
- }
94
-
95
-
96
- /**
97
- * Sets the message for the exception, allowing for custom formatting beyond fException
98
- *
99
- * If this method receives exactly two parameters, a string and an array,
100
- * the string will be used as a message in a HTML `<p>` tag and the array
101
- * will be turned into an unorder list `<ul>` tag with each element in the
102
- * array being an `<li>` tag. It is possible to pass an optional exception
103
- * code as a third parameter.
104
- *
105
- * The following PHP:
106
- *
107
- * {{{
108
- * #!php
109
- * throw new fValidationException(
110
- * 'The following problems were found:',
111
- * array(
112
- * 'Please provide your name',
113
- * 'Please provide your email address'
114
- * )
115
- * );
116
- * }}}
117
- *
118
- * Would create the message:
119
- *
120
- * {{{
121
- * #!text/html
122
- * <p>The following problems were found:</p>
123
- * <ul>
124
- * <li>Please provide your name</li>
125
- * <li>Please provide your email address</li>
126
- * </ul>
127
- * }}}
128
- *
129
- * If the parameters are anything else, they will be passed to
130
- * fException::__construct().
131
- *
132
- * @param string $message The beginning message for the exception. This will be placed in a `<p>` tag.
133
- * @param array $sub_messages An array of strings to place in a `<ul>` tag
134
- * @param mixed $code The optional exception code
135
- * @return fException
136
- */
137
- public function __construct($message='')
138
- {
139
- $params = func_get_args();
140
-
141
- if ((count($params) == 2 || count($params) == 3) && is_string($params[0]) && is_array($params[1])) {
142
-
143
-
144
- $message = sprintf(
145
- "<p>%1\$s</p>\n<ul>\n<li>%2\$s</li>\n</ul>",
146
- self::compose($params[0]),
147
- join("</li>\n<li>", $this->formatErrorArray($params[1]))
148
- );
149
-
150
- $params = array_merge(
151
- // This escapes % signs since fException is going to look for sprintf formatting codes
152
- array(str_replace('%', '%%', $message)),
153
- // This grabs the exception code if one is defined
154
- array_slice($params, 2)
155
- );
156
- }
157
-
158
- call_user_func_array(
159
- array($this, 'fException::__construct'),
160
- $params
161
- );
162
- }
163
-
164
-
165
- /**
166
- * Takes an error array that may or may not be nested and returns a HTML string representation
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.13
8
  Author: Wayne Allen
9
  Author URI: http://PostiePlugin.com/
10
  License: GPL2
@@ -28,7 +28,7 @@
28
  */
29
 
30
  /*
31
- $Id: postie.php 1525592 2016-10-31 21:19:50Z WayneAllen $
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.13');
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.13
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