1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Class to parse and email in to its header and body parts for use in posting |
5
|
|
|
* |
6
|
|
|
* @name ElkArte Forum |
7
|
|
|
* @copyright ElkArte Forum contributors |
8
|
|
|
* @license BSD http://opensource.org/licenses/BSD-3-Clause |
9
|
|
|
* |
10
|
|
|
* @version 2.0 dev |
11
|
|
|
* |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Class to parse and email in to its header and body parts for use in posting |
16
|
|
|
* |
17
|
|
|
* What it does: |
18
|
|
|
* |
19
|
|
|
* - Can read from a supplied string, stdin or from the failed email database |
20
|
|
|
* - Parses and decodes headers, return them in a named array $headers |
21
|
|
|
* - Parses, decodes and translates message body returns body and plain_body sections |
22
|
|
|
* - Parses and decodes attachments returns attachments and inline_files |
23
|
|
|
* |
24
|
|
|
* Load class |
25
|
|
|
* Initiate as |
26
|
|
|
* - $email_message = new EmailParse(); |
27
|
|
|
* |
28
|
|
|
* Make the call, loads data and performs all need parsing |
29
|
|
|
* - $email_message->read_email(true); // Read data and parse it, prefer html section |
30
|
|
|
* |
31
|
|
|
* Just load data: |
32
|
|
|
* - $email_message->read_data(); // load data from stdin |
33
|
|
|
* - $email_message->read_data($data); // load data from a supplied string |
34
|
|
|
* |
35
|
|
|
* Get some email details: |
36
|
|
|
* - $email_message->headers // All the headers in an array |
37
|
|
|
* - $email_message->body // The decoded / translated message |
38
|
|
|
* - $email_message->raw_message // The entire message w/headers as read |
39
|
|
|
* - $email_message->plain_body // The plain text version of the message |
40
|
|
|
* - $email_message->attachments // Any attachments with key = filename |
41
|
|
|
* |
42
|
|
|
* Optional functions: |
43
|
|
|
* - $email_message->load_address(); // Returns array with to/from/cc addresses |
44
|
|
|
* - $email_message->load_key(); // Returns the security key is found, also sets |
45
|
|
|
* message_key, message_type and message_id |
46
|
|
|
* - $email_message->load_spam(); // Returns boolean on if spam headers are set |
47
|
|
|
* - $email_message->load_ip(); // Set ip origin of the email if available |
48
|
|
|
* - $email_message->load_returnpath(); // Load the message return path |
49
|
|
|
* |
50
|
|
|
* @package Maillist |
51
|
|
|
*/ |
52
|
|
|
class Email_Parse |
53
|
|
|
{ |
54
|
|
|
/** |
55
|
|
|
* The full message section (headers, body, etc) we are working on |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
public $raw_message = null; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Attachments found after the message |
62
|
|
|
* @var string[] |
63
|
|
|
*/ |
64
|
|
|
public $attachments = array(); |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Attachments that we designated as inline with the text |
68
|
|
|
* @var string[] |
69
|
|
|
*/ |
70
|
|
|
public $inline_files = array(); |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Parsed and decoded message body, may be plain text or html |
74
|
|
|
* @var string |
75
|
|
|
*/ |
76
|
|
|
public $body = null; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Parsed and decoded message body, only plain text version |
80
|
|
|
* @var string |
81
|
|
|
*/ |
82
|
|
|
public $plain_body = null; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* All of the parsed message headers |
86
|
|
|
* @var mixed[] |
87
|
|
|
*/ |
88
|
|
|
public $headers = array(); |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Full security key |
92
|
|
|
* @var string |
93
|
|
|
*/ |
94
|
|
|
public $message_key_id = null; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Message hex-code |
98
|
|
|
* @var string |
99
|
|
|
*/ |
100
|
|
|
public $message_key = null; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Message type of the key p, m or t |
104
|
|
|
* @var string |
105
|
|
|
*/ |
106
|
|
|
public $message_type = null; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* If an html was found in the message |
110
|
|
|
* @var boolean |
111
|
|
|
*/ |
112
|
|
|
public $html_found = false; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* If any positive spam headers were found in the message |
116
|
|
|
* @var boolean |
117
|
|
|
*/ |
118
|
|
|
public $spam_found = false; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Message id of the key |
122
|
|
|
* @var int |
123
|
|
|
*/ |
124
|
|
|
public $message_id = null; |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Holds the return path as set in the email header |
128
|
|
|
* @var string |
129
|
|
|
*/ |
130
|
|
|
public $return_path = null; |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Holds the message subject |
134
|
|
|
* @var string |
135
|
|
|
*/ |
136
|
|
|
public $subject = null; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Holds the email to from & cc emails and names |
140
|
|
|
* @var mixed[] |
141
|
|
|
*/ |
142
|
|
|
public $email = array(); |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Holds the sending ip of the email |
146
|
|
|
* @var string|boolean |
147
|
|
|
*/ |
148
|
|
|
public $ip = false; |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* If the file was converted to utf8 |
152
|
|
|
* @var boolean |
153
|
|
|
*/ |
154
|
|
|
public $_converted_utf8 = false; |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Whether the message is a DSN (Delivery Status Notification - aka "bounce"), |
158
|
|
|
* indicating failed delivery |
159
|
|
|
* @var boolean |
160
|
|
|
*/ |
161
|
|
|
public $_is_dsn = false; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Holds the field/value/type report codes from DSN messages |
165
|
|
|
* Accessible as [$field]['type'] and [$field]['value'] |
166
|
|
|
* @var mixed[] |
167
|
|
|
*/ |
168
|
|
|
public $_dsn = null; |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Holds the current email address, to, from, cc |
172
|
|
|
* @var string |
173
|
|
|
*/ |
174
|
|
|
private $_email_address = null; |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Holds the current email name |
178
|
|
|
* @var string |
179
|
|
|
*/ |
180
|
|
|
private $_email_name = null; |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Holds each boundary section of the message |
184
|
|
|
* @var string[] |
185
|
|
|
*/ |
186
|
|
|
private $_boundary_section = array(); |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* The total number of boundary sections |
190
|
|
|
* @var int |
191
|
|
|
*/ |
192
|
|
|
private $_boundary_section_count = 0; |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* The message header block |
196
|
|
|
* @var string |
197
|
|
|
*/ |
198
|
|
|
private $_header_block = null; |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Loads an email message from stdin, file or from a supplied string |
202
|
|
|
* |
203
|
|
|
* @param string $data optional, if supplied must be a full headers+body email string |
204
|
|
|
* @param string $location optional, used for debug |
205
|
|
|
* @throws Elk_Exception |
206
|
|
|
*/ |
207
|
1 |
|
public function read_data($data = '', $location = '') |
208
|
|
|
{ |
209
|
|
|
// Supplied a string of data, simply use it |
210
|
1 |
|
if ($data !== null) |
211
|
|
|
{ |
212
|
1 |
|
$this->raw_message = !empty($data) ? $data : false; |
|
|
|
|
213
|
|
|
} |
214
|
|
|
// Not running from the CLI, must be from the ACP |
215
|
|
|
elseif (!defined('STDIN')) |
216
|
|
|
{ |
217
|
|
|
$this->_readFailed($location); |
218
|
|
|
} |
219
|
|
|
// Load file data straight from the pipe |
220
|
|
|
else |
221
|
|
|
{ |
222
|
|
|
$this->raw_message = file_get_contents('php://stdin'); |
223
|
|
|
} |
224
|
1 |
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Load a message for parsing by reading it from the DB or from a debug file |
228
|
|
|
* |
229
|
|
|
* - Must have admin permissions |
230
|
|
|
* |
231
|
|
|
* @param string $location |
232
|
|
|
* @throws Elk_Exception |
233
|
|
|
*/ |
234
|
|
|
private function _readFailed($location) |
235
|
|
|
{ |
236
|
|
|
// Called from the ACP, you must have approve permissions |
237
|
|
|
if (isset($_POST['item'])) |
238
|
|
|
{ |
239
|
|
|
isAllowedTo(array('admin_forum', 'approve_emails')); |
240
|
|
|
|
241
|
|
|
// Read in the file from the failed log table |
242
|
|
|
$this->raw_message = $this->_query_load_email($_POST['item']); |
243
|
|
|
} |
244
|
|
|
// Debugging file, just used for testing |
245
|
|
|
elseif (file_exists($location . '/elk-test.eml')) |
246
|
|
|
{ |
247
|
|
|
isAllowedTo('admin_forum'); |
248
|
|
|
$this->raw_message = file_get_contents($location . '/elk-test.eml'); |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Loads an email message from the database |
254
|
|
|
* |
255
|
|
|
* @param int $id id of the email to retrieve from the failed log |
256
|
|
|
* |
257
|
|
|
* @return string |
258
|
|
|
*/ |
259
|
|
|
private function _query_load_email($id) |
260
|
|
|
{ |
261
|
|
|
$db = database(); |
262
|
|
|
|
263
|
|
|
// Nothing to load then |
264
|
|
|
if (empty($id)) |
265
|
|
|
{ |
266
|
|
|
return ''; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
$request = $db->query('', ' |
270
|
|
|
SELECT message |
271
|
|
|
FROM {db_prefix}postby_emails_error |
272
|
|
|
WHERE id_email = {int:id} |
273
|
|
|
LIMIT 1', |
274
|
|
|
array( |
275
|
|
|
'id' => $id |
276
|
|
|
) |
277
|
|
|
); |
278
|
|
|
list ($message) = $db->fetch_row($request); |
279
|
|
|
$db->free_result($request); |
280
|
|
|
|
281
|
|
|
return $message; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Main email routine, calls the needed functions to parse the data so that |
286
|
|
|
* its available. |
287
|
|
|
* |
288
|
|
|
* What it does: |
289
|
|
|
* |
290
|
|
|
* - read/load data |
291
|
|
|
* - split headers from the body |
292
|
|
|
* - break header string in to individual header keys |
293
|
|
|
* - determine content type and character encoding |
294
|
|
|
* - convert message body's |
295
|
|
|
* |
296
|
|
|
* @param boolean $html - flag to determine if we are saving html or not |
297
|
|
|
* @param string $data - full header+message string |
298
|
|
|
* @param string $location - optional, used for debug |
299
|
|
|
* @throws Elk_Exception |
300
|
|
|
*/ |
301
|
1 |
|
public function read_email($html = false, $data = '', $location = '') |
302
|
|
|
{ |
303
|
|
|
// Main, will read, split, parse, decode an email |
304
|
1 |
|
$this->read_data($data, $location); |
305
|
|
|
|
306
|
1 |
|
if ($this->raw_message) |
307
|
|
|
{ |
308
|
1 |
|
$this->_split_headers(); |
309
|
1 |
|
$this->_parse_headers(); |
310
|
1 |
|
$this->_parse_content_headers(); |
311
|
1 |
|
$this->_parse_body($html); |
312
|
1 |
|
$this->load_subject(); |
313
|
1 |
|
$this->_is_dsn = $this->_check_dsn(); |
314
|
|
|
} |
315
|
1 |
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Separate the email message headers from the message body |
319
|
|
|
* |
320
|
|
|
* The header is separated from the body by |
321
|
|
|
* - 1 the first empty line or |
322
|
|
|
* - 2 a line that does not start with a tab, a field name followed by a colon or a space |
323
|
|
|
*/ |
324
|
1 |
|
private function _split_headers() |
325
|
|
|
{ |
326
|
1 |
|
$this->_header_block = ''; |
327
|
1 |
|
$match = array(); |
328
|
|
|
|
329
|
|
|
// Do we even start with a header in this boundary section? |
330
|
1 |
|
if (!preg_match('~^[\w-]+:[ ].*?\r?\n~i', $this->raw_message)) |
331
|
|
|
{ |
332
|
1 |
|
return; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
// The header block ends based on condition (1) or (2) |
336
|
1 |
|
if (!preg_match('~^(.*?)\r?\n(?:\r?\n|(?!(\t|[\w-]+:|[ ])))(.*)~s', $this->raw_message, $match)) |
337
|
|
|
{ |
338
|
|
|
return; |
339
|
|
|
} |
340
|
|
|
|
341
|
1 |
|
$this->_header_block = $match[1]; |
342
|
1 |
|
$this->body = $match[3]; |
343
|
1 |
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Takes the header block created with _split_headers and separates it |
347
|
|
|
* in to header keys => value pairs |
348
|
|
|
*/ |
349
|
1 |
|
private function _parse_headers() |
350
|
|
|
{ |
351
|
|
|
// Remove windows style \r's |
352
|
1 |
|
$this->_header_block = str_replace("\r\n", "\n", $this->_header_block); |
353
|
|
|
|
354
|
|
|
// unfolding multi-line headers, a CRLF immediately followed by a LWSP-char is equivalent to the LWSP-char |
355
|
1 |
|
$this->_header_block = preg_replace("~\n(\t| )+~", ' ', $this->_header_block); |
356
|
|
|
|
357
|
|
|
// Build the array of headers |
358
|
1 |
|
$headers = explode("\n", trim($this->_header_block)); |
359
|
1 |
|
foreach ($headers as $header) |
360
|
|
|
{ |
361
|
1 |
|
$pos = strpos($header, ':'); |
362
|
1 |
|
$header_key = substr($header, 0, $pos); |
363
|
1 |
|
$pos++; |
364
|
|
|
|
365
|
|
|
// Invalid, empty or generally malformed header |
366
|
1 |
|
if (!$header_key || $pos === strlen($header) || ($header[$pos] !== ' ' && $header[$pos] !== "\t")) |
367
|
|
|
{ |
368
|
1 |
|
continue; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
// The header key (standardized) and value |
372
|
1 |
|
$header_value = substr($header, $pos + 1); |
373
|
1 |
|
$header_key = strtolower(trim($header_key)); |
374
|
|
|
|
375
|
|
|
// Decode and add it in to our headers array |
376
|
1 |
|
if (!isset($this->headers[$header_key])) |
377
|
|
|
{ |
378
|
1 |
|
$this->headers[$header_key] = $this->_decode_header($header_value); |
379
|
|
|
} |
380
|
|
|
else |
381
|
|
|
{ |
382
|
1 |
|
$this->headers[$header_key] .= ' ' . $this->_decode_header($header_value); |
383
|
|
|
} |
384
|
|
|
} |
385
|
1 |
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Content headers need to be set so we can properly decode the message body. |
389
|
|
|
* |
390
|
|
|
* What it does: |
391
|
|
|
* |
392
|
|
|
* - Content headers often use the optional parameter value syntax which need to be |
393
|
|
|
* specially processed. |
394
|
|
|
* - Parses or sets defaults for the following: |
395
|
|
|
* content-type, content-disposition, content-transfer-encoding |
396
|
|
|
*/ |
397
|
1 |
|
private function _parse_content_headers() |
398
|
|
|
{ |
399
|
|
|
// What kind of message content do we have |
400
|
1 |
|
if (isset($this->headers['content-type'])) |
401
|
|
|
{ |
402
|
1 |
|
$this->_parse_content_header_parameters($this->headers['content-type'], 'content-type'); |
403
|
1 |
View Code Duplication |
if (empty($this->headers['x-parameters']['content-type']['charset'])) |
404
|
|
|
{ |
405
|
1 |
|
$this->headers['x-parameters']['content-type']['charset'] = 'UTF-8'; |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
View Code Duplication |
else |
409
|
|
|
{ |
410
|
|
|
// No content header given so we assume plain text |
411
|
1 |
|
$this->headers['content-type'] = 'text/plain'; |
412
|
1 |
|
$this->headers['x-parameters']['content-type']['charset'] = 'UTF-8'; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
// Any special content or assume standard inline |
416
|
1 |
|
if (isset($this->headers['content-disposition'])) |
417
|
|
|
{ |
418
|
|
|
$this->_parse_content_header_parameters($this->headers['content-disposition'], 'content-disposition'); |
419
|
|
|
} |
420
|
|
|
else |
421
|
|
|
{ |
422
|
1 |
|
$this->headers['content-disposition'] = 'inline'; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
// How this message been encoded, utf8, quoted printable, other??, if none given assume standard 7bit |
426
|
1 |
|
if (isset($this->headers['content-transfer-encoding'])) |
427
|
|
|
{ |
428
|
1 |
|
$this->_parse_content_header_parameters($this->headers['content-transfer-encoding'], 'content-transfer-encoding'); |
429
|
|
|
} |
430
|
|
|
else |
431
|
|
|
{ |
432
|
1 |
|
$this->headers['content-transfer-encoding'] = '7bit'; |
433
|
|
|
} |
434
|
1 |
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Checks if a given header has any optional parameter values |
438
|
|
|
* |
439
|
|
|
* A header like Content-type: text/plain; charset=iso-8859-1 will become |
440
|
|
|
* - headers[Content-type] = text/plain |
441
|
|
|
* - headers['x-parameters'][charset] = iso-8859-1 |
442
|
|
|
* |
443
|
|
|
* If parameters are found, sets the primary value to the given key and the additional |
444
|
|
|
* values are placed to our catch all x-parameters key. Done this way to prevent |
445
|
|
|
* overwriting a primary header key with a secondary one |
446
|
|
|
* |
447
|
|
|
* @param string $value |
448
|
|
|
* @param string $key |
449
|
|
|
*/ |
450
|
1 |
|
private function _parse_content_header_parameters($value, $key) |
451
|
|
|
{ |
452
|
1 |
|
$matches = array(); |
453
|
|
|
|
454
|
|
|
// Does the header key contain parameter values? |
455
|
1 |
|
$pos = strpos($value, ';'); |
456
|
1 |
|
if ($pos !== false) |
457
|
|
|
{ |
458
|
|
|
// Assign the primary value to the key |
459
|
1 |
|
$this->headers[$key] = strtolower(trim(substr($value, 0, $pos))); |
460
|
|
|
|
461
|
|
|
// Place any parameter values in the x-parameters key |
462
|
1 |
|
$parameters = ltrim(substr($value, $pos + 1)); |
463
|
1 |
|
if (!empty($parameters) && preg_match_all('~([A-Za-z-]+)="?(.*?)"?\s*(?:;|$)~', $parameters, $matches)) |
464
|
|
|
{ |
465
|
1 |
|
$count = count($matches[0]); |
466
|
1 |
|
for ($i = 0; $i < $count; $i++) |
467
|
|
|
{ |
468
|
1 |
|
$this->headers['x-parameters'][$key][strtolower($matches[1][$i])] = $matches[2][$i]; |
469
|
|
|
} |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
// No parameters associated with this header |
473
|
|
|
else |
474
|
|
|
{ |
475
|
1 |
|
$this->headers[$key] = strtolower(trim($value)); |
476
|
|
|
} |
477
|
1 |
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Based on the the message content type, determine how to best proceed |
481
|
|
|
* |
482
|
|
|
* @param boolean $html |
483
|
|
|
*/ |
484
|
1 |
|
private function _parse_body($html = false) |
485
|
|
|
{ |
486
|
|
|
// based on the content type for this body, determine what do do |
487
|
1 |
|
switch ($this->headers['content-type']) |
488
|
|
|
{ |
489
|
|
|
// The text/plain content type is the generic subtype for plain text. It is the default specified by RFC 822. |
490
|
1 |
|
case 'text/plain': |
491
|
1 |
|
$this->body = $this->_decode_string($this->body, $this->headers['content-transfer-encoding'], $this->headers['x-parameters']['content-type']['charset']); |
|
|
|
|
492
|
1 |
|
$this->plain_body = $this->body; |
|
|
|
|
493
|
1 |
|
break; |
494
|
|
|
// The text/html content type is an Internet Media Type as well as a MIME content type. |
495
|
1 |
View Code Duplication |
case 'text/html': |
496
|
1 |
|
$this->html_found = true; |
497
|
1 |
|
$this->body = $this->_decode_string($this->body, $this->headers['content-transfer-encoding'], $this->headers['x-parameters']['content-type']['charset']); |
498
|
1 |
|
break; |
499
|
|
|
// We don't process the following, noted here so people know why |
500
|
|
|
// |
501
|
|
|
// multipart/digest - used to send collections of plain-text messages |
502
|
|
|
// multipart/byteranges - defined as a part of the HTTP message protocol. It includes two or more parts, |
503
|
|
|
// each with its own Content-Type and Content-Range fields |
504
|
|
|
// multipart/form-data - intended to allow information providers to express file upload requests uniformly |
505
|
|
|
// text/enriched - Uses a very limited set of formatting commands all with <command name></command name> |
506
|
|
|
// text/richtext - Obsolete version of the above |
507
|
|
|
// |
508
|
1 |
|
case 'multipart/digest': |
509
|
1 |
|
case 'multipart/byteranges': |
510
|
1 |
|
case 'multipart/form-data': |
511
|
1 |
|
case 'text/enriched': |
512
|
1 |
|
case 'text/richtext': |
513
|
|
|
break; |
514
|
|
|
// The following are considered multi part messages, as such they *should* contain several sections each |
515
|
|
|
// representing the same message in various ways such as plain text (mandatory), html section, and |
516
|
|
|
// encoded section such as quoted printable as well as attachments both as files and inline |
517
|
|
|
// |
518
|
|
|
// multipart/alternative - the same information is presented in different body parts in different forms. |
519
|
|
|
// The body parts are ordered by increasing complexity and accuracy |
520
|
|
|
// multipart/mixed - used when the body parts are independent and need to be bundled in a particular order |
521
|
|
|
// multipart/parallel - display all of the parts simultaneously on hardware and software that can do so (image with audio) |
522
|
|
|
// multipart/related - used for compound documents, those messages in which the separate body parts are intended to work |
523
|
|
|
// together to provide the full meaning of the message |
524
|
|
|
// multipart/report - defined for returning delivery status reports, with optional included messages |
525
|
|
|
// multipart/signed -provides a security framework for MIME parts |
526
|
|
|
// multipart/encrypted - as above provides a security framework for MIME parts |
527
|
|
|
// message/rfc822 - used to enclose a complete message within a message |
528
|
|
|
// |
529
|
1 |
|
case 'multipart/alternative': |
530
|
|
|
case 'multipart/mixed': |
531
|
|
|
case 'multipart/parallel': |
532
|
|
|
case 'multipart/related': |
533
|
|
|
case 'multipart/report': |
534
|
|
|
case 'multipart/signed': |
535
|
|
|
case 'multipart/encrypted': |
536
|
|
|
case 'message/rfc822': |
537
|
1 |
|
if (!isset($this->headers['x-parameters']['content-type']['boundary'])) |
538
|
|
|
{ |
539
|
|
|
// No boundary's but presented as multipart?, then we must have a incomplete message |
540
|
|
|
$this->body = ''; |
541
|
|
|
return; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
// Break up the message on the boundary --sections, each boundary section will have its |
545
|
|
|
// own Content Type and Encoding and we will process each as such |
546
|
1 |
|
$this->_boundary_split($this->headers['x-parameters']['content-type']['boundary'], $html); |
547
|
|
|
|
548
|
|
|
// Some multi-part messages ... are singletons :P |
549
|
1 |
|
if ($this->_boundary_section_count === 1) |
550
|
|
|
{ |
551
|
|
|
$this->body = $this->_boundary_section[0]->body; |
552
|
|
|
$this->headers['x-parameters'] = $this->_boundary_section[0]->headers['x-parameters']; |
553
|
|
|
} |
554
|
|
|
// We found multiple sections, lets go through each |
555
|
1 |
|
elseif ($this->_boundary_section_count > 1) |
556
|
|
|
{ |
557
|
1 |
|
$html_ids = array(); |
558
|
1 |
|
$text_ids = array(); |
559
|
1 |
|
$this->body = ''; |
560
|
1 |
|
$this->plain_body = empty($this->plain_body) ? '' : $this->plain_body; |
561
|
1 |
|
$bypass = array('application/pgp-encrypted', 'application/pgp-signature', 'application/pgp-keys'); |
562
|
|
|
|
563
|
|
|
// Go through each boundary section |
564
|
1 |
|
for ($i = 0; $i < $this->_boundary_section_count; $i++) |
565
|
|
|
{ |
566
|
|
|
// Stuff we can't or don't want to process |
567
|
1 |
|
if (in_array($this->_boundary_section[$i]->headers['content-type'], $bypass)) |
568
|
|
|
{ |
569
|
|
|
continue; |
570
|
|
|
} |
571
|
|
|
// HTML sections |
572
|
1 |
|
elseif ($this->_boundary_section[$i]->headers['content-type'] === 'text/html') |
573
|
|
|
{ |
574
|
1 |
|
$html_ids[] = $i; |
575
|
|
|
} |
576
|
|
|
// Plain section |
577
|
1 |
|
elseif ($this->_boundary_section[$i]->headers['content-type'] === 'text/plain') |
578
|
|
|
{ |
579
|
1 |
|
$text_ids[] = $i; |
580
|
|
|
} |
581
|
|
|
// Message is a DSN (Delivery Status Notification) |
582
|
|
|
elseif ($this->_boundary_section[$i]->headers['content-type'] === 'message/delivery-status') |
583
|
|
|
{ |
584
|
|
|
$this->_process_DSN($i); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
// Attachments, we love em |
588
|
1 |
|
$this->_process_attachments($i); |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
// We always return a plain text version for use |
592
|
1 |
|
if (!empty($text_ids)) |
593
|
|
|
{ |
594
|
1 |
|
foreach ($text_ids as $id) |
595
|
|
|
{ |
596
|
1 |
|
$this->plain_body .= $this->_boundary_section[$id]->body; |
597
|
|
|
} |
598
|
|
|
} |
599
|
|
|
elseif (!empty($html_ids)) |
600
|
|
|
{ |
601
|
|
|
// This should never run as emails should always have a plain text section to be valid, still ... |
602
|
|
|
foreach ($html_ids as $id) |
603
|
|
|
{ |
604
|
|
|
$this->plain_body .= $this->_boundary_section[$id]->body; |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
$this->plain_body = str_ireplace('<p>', "\n\n", $this->plain_body); |
608
|
|
|
$this->plain_body = str_ireplace(array('<br />', '<br>', '</p>', '</div>'), "\n", $this->plain_body); |
609
|
|
|
$this->plain_body = strip_tags($this->plain_body); |
610
|
|
|
} |
611
|
1 |
|
$this->plain_body = $this->_decode_body($this->plain_body); |
|
|
|
|
612
|
|
|
|
613
|
|
|
// If they want the html section, and its available, we need to set it |
614
|
1 |
|
if ($html && !empty($html_ids)) |
615
|
|
|
{ |
616
|
1 |
|
$this->html_found = true; |
617
|
1 |
|
$text_ids = $html_ids; |
618
|
|
|
} |
619
|
|
|
|
620
|
1 |
|
if (!empty($text_ids)) |
621
|
|
|
{ |
622
|
|
|
// For all the chosen sections |
623
|
1 |
|
foreach ($text_ids as $id) |
624
|
|
|
{ |
625
|
1 |
|
$this->body .= $this->_boundary_section[$id]->body; |
626
|
|
|
|
627
|
|
|
// A section may have its own attachments if it had is own unique boundary sections |
628
|
|
|
// so we need to check and add them in as needed |
629
|
1 |
|
foreach ($this->_boundary_section[$id]->attachments as $key => $value) |
630
|
|
|
{ |
631
|
|
|
$this->attachments[$key] = $value; |
632
|
|
|
} |
633
|
|
|
|
634
|
1 |
|
foreach ($this->_boundary_section[$id]->inline_files as $key => $value) |
635
|
|
|
{ |
636
|
1 |
|
$this->inline_files[$key] = $value; |
637
|
|
|
} |
638
|
|
|
} |
639
|
1 |
|
$this->body = $this->_decode_body($this->body); |
|
|
|
|
640
|
|
|
|
641
|
|
|
// Return the right set of x-parameters and content type for the body we are returning |
642
|
1 |
View Code Duplication |
if (isset($this->_boundary_section[$text_ids[0]]->headers['x-parameters'])) |
643
|
|
|
{ |
644
|
1 |
|
$this->headers['x-parameters'] = $this->_boundary_section[$text_ids[0]]->headers['x-parameters']; |
645
|
|
|
} |
646
|
|
|
|
647
|
1 |
|
$this->headers['content-type'] = $this->_boundary_section[$text_ids[0]]->headers['content-type']; |
648
|
|
|
} |
649
|
|
|
} |
650
|
1 |
|
break; |
651
|
|
View Code Duplication |
default: |
652
|
|
|
// deal with all the rest (e.g. image/xyx) the standard way |
653
|
|
|
$this->body = $this->_decode_string($this->body, $this->headers['content-transfer-encoding'], $this->headers['x-parameters']['content-type']['charset']); |
654
|
|
|
break; |
655
|
|
|
} |
656
|
1 |
|
} |
657
|
|
|
|
658
|
|
|
/** |
659
|
|
|
* If the boundary is a failed email response, set the DSN flag for the admin |
660
|
|
|
* |
661
|
|
|
* @param int $i The section being worked |
662
|
|
|
*/ |
663
|
|
|
private function _process_DSN($i) |
664
|
|
|
{ |
665
|
|
|
// These sections often have extra blank lines, so cannot be counted on to be |
666
|
|
|
// fully accessible in ->headers. The "body" of this section contains values |
667
|
|
|
// formatted by FIELD: [TYPE;] VALUE |
668
|
|
|
$dsn_body = array(); |
669
|
|
|
foreach (explode("\n", str_replace("\r\n", "\n", $this->_boundary_section[$i]->body)) as $line) |
670
|
|
|
{ |
671
|
|
|
$type = ''; |
672
|
|
|
list($field, $rest) = explode(':', $line); |
673
|
|
|
|
674
|
|
View Code Duplication |
if (strpos($line, ';')) |
675
|
|
|
{ |
676
|
|
|
list ($type, $val) = explode(';', $rest); |
677
|
|
|
} |
678
|
|
|
else |
679
|
|
|
{ |
680
|
|
|
$val = $rest; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
$dsn_body[trim(strtolower($field))] = array('type' => trim($type), 'value' => trim($val)); |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
switch ($dsn_body['action']['value']) |
687
|
|
|
{ |
688
|
|
|
case 'delayed': |
689
|
|
|
// Remove this if we don't want to flag delayed delivery addresses as "dirty" |
690
|
|
|
// May be caused by temporary net failures, e.g. DNS outage |
691
|
|
|
// Lack of break is intentional |
692
|
|
|
case 'failed': |
693
|
|
|
// The email failed to be delivered. |
694
|
|
|
$this->_is_dsn = true; |
695
|
|
|
$this->_dsn = array('headers' => $this->_boundary_section[$i]->headers, 'body' => $dsn_body); |
|
|
|
|
696
|
|
|
break; |
697
|
|
|
default: |
698
|
|
|
$this->_is_dsn = false; |
699
|
|
|
} |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* If the boundary section is "attachment" or "inline", process and save the data |
704
|
|
|
* |
705
|
|
|
* - Data is saved in ->attachments or ->inline_files |
706
|
|
|
* |
707
|
|
|
* @param int $i The section being worked |
708
|
|
|
*/ |
709
|
1 |
|
private function _process_attachments($i) |
710
|
|
|
{ |
711
|
1 |
|
if ($this->_boundary_section[$i]->headers['content-disposition'] === 'attachment' || $this->_boundary_section[$i]->headers['content-disposition'] === 'inline' || isset($this->_boundary_section[$i]->headers['content-id'])) |
712
|
|
|
{ |
713
|
|
|
// Get the attachments file name |
714
|
1 |
|
if (isset($this->_boundary_section[$i]->headers['x-parameters']['content-disposition']['filename'])) |
715
|
|
|
{ |
716
|
|
|
$file_name = $this->_boundary_section[$i]->headers['x-parameters']['content-disposition']['filename']; |
717
|
|
|
} |
718
|
1 |
View Code Duplication |
elseif (isset($this->_boundary_section[$i]->headers['x-parameters']['content-type']['name'])) |
719
|
|
|
{ |
720
|
|
|
$file_name = $this->_boundary_section[$i]->headers['x-parameters']['content-type']['name']; |
721
|
|
|
} |
722
|
|
|
else |
723
|
|
|
{ |
724
|
1 |
|
return; |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
// Load the attachment data |
728
|
|
|
$this->attachments[$file_name] = $this->_boundary_section[$i]->body; |
729
|
|
|
|
730
|
|
|
// Inline attachments are a bit more complicated. |
731
|
|
|
if (isset($this->_boundary_section[$i]->headers['content-id']) && $this->_boundary_section[$i]->headers['content-disposition'] === 'inline') |
732
|
|
|
{ |
733
|
|
|
$this->inline_files[$file_name] = trim($this->_boundary_section[$i]->headers['content-id'], ' <>'); |
734
|
|
|
} |
735
|
|
|
} |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* Split up multipart messages and process each section separately |
740
|
|
|
* as its own email object |
741
|
|
|
* |
742
|
|
|
* @param string $boundary |
743
|
|
|
* @param boolean $html - flag to indicate html content |
744
|
|
|
*/ |
745
|
1 |
|
private function _boundary_split($boundary, $html) |
746
|
|
|
{ |
747
|
|
|
// Split this message up on its boundary sections |
748
|
1 |
|
$parts = explode('--' . $boundary, $this->body); |
749
|
1 |
|
foreach ($parts as $part) |
750
|
|
|
{ |
751
|
1 |
|
$part = trim($part); |
752
|
|
|
|
753
|
|
|
// Nothing? |
754
|
1 |
|
if (empty($part)) |
755
|
|
|
{ |
756
|
|
|
continue; |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
// Parse this section just like its was a separate email |
760
|
1 |
|
$this->_boundary_section[$this->_boundary_section_count] = new Email_Parse(); |
761
|
1 |
|
$this->_boundary_section[$this->_boundary_section_count]->read_email($html, $part); |
762
|
|
|
|
763
|
1 |
|
$this->plain_body .= $this->_boundary_section[$this->_boundary_section_count]->plain_body; |
764
|
|
|
|
765
|
1 |
|
$this->_boundary_section_count++; |
766
|
|
|
} |
767
|
1 |
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Converts a header string to ascii/UTF8 |
771
|
|
|
* |
772
|
|
|
* What it does: |
773
|
|
|
* |
774
|
|
|
* - Headers, mostly subject and names may be encoded as quoted printable or base64 |
775
|
|
|
* to allow for non ascii characters in those fields. |
776
|
|
|
* - This encoding is separate from the message body encoding and must be |
777
|
|
|
* determined since this encoding is not directly specified by the headers themselves |
778
|
|
|
* |
779
|
|
|
* @param string $val |
780
|
|
|
* @param bool $strict |
781
|
|
|
* @return string |
782
|
|
|
*/ |
783
|
1 |
|
private function _decode_header($val, $strict = false) |
784
|
|
|
{ |
785
|
|
|
// Check if this header even needs to be decoded. |
786
|
1 |
|
if (strpos($val, '=?') === false || strpos($val, '?=') === false) |
787
|
|
|
{ |
788
|
1 |
|
return trim($val); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
// If iconv mime is available just use it and be done |
792
|
|
|
if (function_exists('iconv_mime_decode')) |
793
|
|
|
{ |
794
|
|
|
return iconv_mime_decode($val, $strict ? 1 : 2, 'UTF-8'); |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
// The RFC 2047-3 defines an encoded-word as a sequence of characters that |
798
|
|
|
// begins with "=?", ends with "?=", and has two "?"s in between. After the first question mark |
799
|
|
|
// is the name of the character encoding being used; after the second question mark |
800
|
|
|
// is the manner in which it's being encoded into plain ASCII (Q=quoted printable, B=base64); |
801
|
|
|
// and after the third question mark is the text itself. |
802
|
|
|
// Subject: =?iso-8859-1?Q?=A1Hola,_se=F1or!?= |
803
|
|
|
$matches = array(); |
804
|
|
|
if (preg_match_all('~(.*?)(=\?([^?]+)\?(Q|B)\?([^?]*)\?=)([^=\(]*)~i', $val, $matches)) |
805
|
|
|
{ |
806
|
|
|
$decoded = ''; |
807
|
|
|
for ($i = 0, $num = count($matches[4]); $i < $num; $i++) |
808
|
|
|
{ |
809
|
|
|
// [1]leading text, [2]=? to ?=, [3]character set, [4]Q or B, [5]the encoded text [6]trailing text |
810
|
|
|
$leading_text = $matches[1][$i]; |
811
|
|
|
$encoded_charset = $matches[3][$i]; |
812
|
|
|
$encoded_type = strtolower($matches[4][$i]); |
813
|
|
|
$encoded_text = $matches[5][$i]; |
814
|
|
|
$trailing_text = $matches[6][$i]; |
815
|
|
|
|
816
|
|
|
if ($strict) |
817
|
|
|
{ |
818
|
|
|
// Technically the encoded word can only be by itself or in a cname |
819
|
|
|
$check = trim($leading_text); |
820
|
|
|
if ($i === 0 && !empty($check) && $check[0] !== '(') |
821
|
|
|
{ |
822
|
|
|
$decoded .= $matches[0][$i]; |
823
|
|
|
continue; |
824
|
|
|
} |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
// Decode and convert our string |
828
|
|
|
if ($encoded_type === 'q') |
829
|
|
|
{ |
830
|
|
|
$decoded_text = $this->_decode_string(str_replace('_', ' ', $encoded_text), 'quoted-printable', $encoded_charset); |
831
|
|
|
} |
832
|
|
|
elseif ($encoded_type === 'b') |
833
|
|
|
{ |
834
|
|
|
$decoded_text = $this->_decode_string($encoded_text, 'base64', $encoded_charset); |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
// Add back in anything after the closing ?= |
838
|
|
|
if (!empty($encoded_text)) |
839
|
|
|
{ |
840
|
|
|
$decoded_text .= $trailing_text; |
|
|
|
|
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
// Add back in the leading text to the now decoded value |
844
|
|
|
if (!empty($leading_text)) |
845
|
|
|
{ |
846
|
|
|
$decoded_text = $leading_text . $decoded_text; |
847
|
|
|
} |
848
|
|
|
|
849
|
|
|
$decoded .= $decoded_text; |
850
|
|
|
} |
851
|
|
|
$val = $decoded; |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
return trim($val); |
855
|
|
|
} |
856
|
|
|
|
857
|
|
|
/** |
858
|
|
|
* Checks the body text to see if it may need to be further decoded |
859
|
|
|
* |
860
|
|
|
* What it does: |
861
|
|
|
* |
862
|
|
|
* - Sadly whats in the body text is not always what the header claims, or the |
863
|
|
|
* header is just wrong. Copy/paste in to email from other apps etc. |
864
|
|
|
* This does an extra check for quoted printable DNA and if found decodes the |
865
|
|
|
* message as such. |
866
|
|
|
* |
867
|
|
|
* @param string $val |
868
|
|
|
* @return string |
869
|
|
|
*/ |
870
|
1 |
|
private function _decode_body($val) |
871
|
|
|
{ |
872
|
|
|
// The encoding tag can be missing in the headers or just wrong |
873
|
1 |
|
if (preg_match('~(?:=C2|=A0|=D2|=D4|=96){1}~s', $val)) |
874
|
|
|
{ |
875
|
|
|
// Remove /r/n to be just /n |
876
|
|
|
$val = preg_replace('~(=0D=0A)~', "\n", $val); |
877
|
|
|
|
878
|
|
|
// utf8 non breaking space which does not decode right |
879
|
|
|
$val = preg_replace('~(=C2=A0)~', ' ', $val); |
880
|
|
|
|
881
|
|
|
// Smart quotes they will decode to black diamonds or other, but if |
882
|
|
|
// UTF-8 these may be valid non smart quotes |
883
|
|
|
if ($this->headers['x-parameters']['content-type']['charset'] !== 'UTF-8') |
884
|
|
|
{ |
885
|
|
|
$val = str_replace('=D4', "'", $val); |
886
|
|
|
$val = str_replace('=D5', "'", $val); |
887
|
|
|
$val = str_replace('=D2', '"', $val); |
888
|
|
|
$val = str_replace('=D3', '"', $val); |
889
|
|
|
$val = str_replace('=A0', '', $val); |
890
|
|
|
} |
891
|
|
|
$val = $this->_decode_string($val, 'quoted-printable'); |
892
|
|
|
} |
893
|
|
|
// Lines end in the tell tail quoted printable ... wrap and decode |
894
|
1 |
|
elseif (preg_match('~\s=[\r?\n]{1}~s', $val)) |
895
|
|
|
{ |
896
|
|
|
$val = preg_replace('~\s=[\r?\n]{1}~', ' ', $val); |
897
|
|
|
$val = $this->_decode_string($val, 'quoted-printable'); |
898
|
|
|
} |
899
|
|
|
// Lines end in = but not == |
900
|
1 |
|
elseif (preg_match('~((?<!=)=[\r?\n])~s', $val)) |
901
|
|
|
{ |
902
|
|
|
$val = $this->_decode_string($val, 'quoted-printable'); |
903
|
|
|
} |
904
|
|
|
|
905
|
1 |
|
return $val; |
906
|
|
|
} |
907
|
|
|
|
908
|
|
|
/** |
909
|
|
|
* Checks the message components to determine if the message is a DSN |
910
|
|
|
* |
911
|
|
|
* What it does: |
912
|
|
|
* |
913
|
|
|
* - Checks the content of the message, looking for headers and values that |
914
|
|
|
* correlate with the message being a DSN. _parse_body checks for the existence |
915
|
|
|
* of a "message/delivery-status" header |
916
|
|
|
* - As many, many daemons and providers do not adhere to the RFC 3464 |
917
|
|
|
* standard, this function will hold the "special cases" |
918
|
|
|
* |
919
|
|
|
* @return boolean|null |
920
|
|
|
*/ |
921
|
1 |
|
private function _check_dsn() |
922
|
|
|
{ |
923
|
|
|
// If we already know it's a DSN, bug out |
924
|
1 |
|
if ($this->_is_dsn) |
925
|
|
|
{ |
926
|
|
|
return true; |
927
|
|
|
} |
928
|
|
|
|
929
|
|
|
/** Add non-header-based detection **/ |
930
|
1 |
|
} |
931
|
|
|
|
932
|
|
|
/** |
933
|
|
|
* Tries to find the original intended recipient that failed to deliver |
934
|
|
|
* |
935
|
|
|
* What it does: |
936
|
|
|
* |
937
|
|
|
* - Checks the headers of a DSN for the various ways that the intended recipient |
938
|
|
|
* Might have been included in the DSN headers |
939
|
|
|
* |
940
|
|
|
* @return string or null |
941
|
|
|
*/ |
942
|
|
|
public function get_failed_dest() |
943
|
|
|
{ |
944
|
|
|
/** Body->Original-Recipient Header **/ |
945
|
|
|
if (isset($this->_dsn['body']['original-recipient']['value'])) |
946
|
|
|
{ |
947
|
|
|
return $this->_dsn['body']['original-recipient']['value']; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
/** Body->Final-recipient Header **/ |
951
|
|
|
if (isset($this->_dsn['body']['final-recipient']['value'])) |
952
|
|
|
{ |
953
|
|
|
return $this->_dsn['body']['final-recipient']['value']; |
954
|
|
|
} |
955
|
|
|
|
956
|
|
|
return null; |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
/** |
960
|
|
|
* Find the message return_path and well return it |
961
|
|
|
* |
962
|
|
|
* @return string or null |
963
|
|
|
*/ |
964
|
|
|
public function load_returnpath() |
965
|
|
|
{ |
966
|
|
|
$matches = array(); |
967
|
|
|
|
968
|
|
|
// Fetch the return path |
969
|
|
|
if (isset($this->headers['return-path'])) |
970
|
|
|
{ |
971
|
|
|
if (preg_match('~(.*?)<(.*?)>~', $this->headers['return-path'], $matches)) |
972
|
|
|
{ |
973
|
|
|
$this->return_path = trim($matches[2]); |
974
|
|
|
} |
975
|
|
|
} |
976
|
|
|
|
977
|
|
|
return $this->return_path; |
978
|
|
|
} |
979
|
|
|
|
980
|
|
|
/** |
981
|
|
|
* Returns the decoded subject of the email |
982
|
|
|
* |
983
|
|
|
* - Makes sure the subject header is set, if not sets it to '' |
984
|
|
|
* |
985
|
|
|
* @return string or null |
986
|
|
|
*/ |
987
|
1 |
|
public function load_subject() |
988
|
|
|
{ |
989
|
|
|
// Account for those no-subject emails |
990
|
1 |
|
if (!isset($this->headers['subject'])) |
991
|
|
|
{ |
992
|
1 |
|
$this->headers['subject'] = ''; |
993
|
|
|
} |
994
|
|
|
|
995
|
|
|
// Change it to a readable form ... |
996
|
1 |
|
$this->subject = htmlspecialchars($this->_decode_header($this->headers['subject']), ENT_COMPAT, 'UTF-8'); |
997
|
|
|
|
998
|
1 |
|
return (string) $this->subject; |
999
|
|
|
} |
1000
|
|
|
|
1001
|
|
|
/** |
1002
|
|
|
* Check for the message security key in common headers, in-reply-to and references |
1003
|
|
|
* |
1004
|
|
|
* - If the key is not found in the header, will search the message body |
1005
|
|
|
* - If the key is still not found will search the entire input stream |
1006
|
|
|
* - returns the found key or false. If found will also save it in the in-reply-to header |
1007
|
|
|
* |
1008
|
|
|
* @param string $key optional |
1009
|
|
|
* @return string of key or false on failure |
1010
|
|
|
*/ |
1011
|
1 |
|
public function load_key($key = '') |
1012
|
|
|
{ |
1013
|
1 |
|
$regex_key = '~(([a-z0-9]{32})\-(p|t|m)(\d+))~i'; |
1014
|
1 |
|
$match = array(); |
1015
|
|
|
|
1016
|
|
|
// Supplied a key, lets check it |
1017
|
1 |
|
if (!empty($key)) |
1018
|
|
|
{ |
1019
|
|
|
preg_match($regex_key, $key, $match); |
1020
|
|
|
} |
1021
|
|
|
// Otherwise we play find the key |
1022
|
|
|
else |
1023
|
|
|
{ |
1024
|
1 |
|
if (!$this->_load_key_from_headers($regex_key)) |
1025
|
|
|
{ |
1026
|
1 |
|
$this->_load_key_from_body(); |
1027
|
|
|
} |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
1 |
|
return !empty($this->message_key_id) ? $this->message_key_id : false; |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
/** |
1034
|
|
|
* Searches the most common locations for the security key |
1035
|
|
|
* |
1036
|
|
|
* - Normal return location would be in the in-reply-to header |
1037
|
|
|
* - Common for it to be shifted to a reference header |
1038
|
|
|
* |
1039
|
|
|
* @param string $regex_key |
1040
|
|
|
* |
1041
|
|
|
* @return bool is the security key is found or not |
1042
|
|
|
*/ |
1043
|
1 |
|
private function _load_key_from_headers($regex_key) |
1044
|
|
|
{ |
1045
|
1 |
|
$found_key = false; |
1046
|
|
|
|
1047
|
|
|
// Check our reply_to_msg_id based on in-reply-to and references, the key *should* be there. |
1048
|
1 |
|
if (empty($this->headers['in-reply-to']) || preg_match($regex_key, $this->headers['in-reply-to'], $match) === 0) |
1049
|
|
|
{ |
1050
|
|
|
// Check if references are set, sometimes email clients thread from there |
1051
|
1 |
|
if (!empty($this->headers['references'])) |
1052
|
|
|
{ |
1053
|
|
|
// Maybe our security key is in the references |
1054
|
1 |
|
$refs = explode(' ', $this->headers['references']); |
1055
|
1 |
|
foreach ($refs as $ref) |
1056
|
|
|
{ |
1057
|
1 |
View Code Duplication |
if (preg_match($regex_key, $ref, $match)) |
1058
|
|
|
{ |
1059
|
|
|
// Found the key in the ref, set the in-reply-to |
1060
|
|
|
$this->headers['in-reply-to'] = $match[1]; |
1061
|
|
|
$this->_load_key_details($match); |
1062
|
|
|
$found_key = true; |
1063
|
1 |
|
break; |
1064
|
|
|
} |
1065
|
|
|
} |
1066
|
|
|
} |
1067
|
|
|
} |
1068
|
|
|
else |
1069
|
|
|
{ |
1070
|
|
|
$this->_load_key_details($match); |
1071
|
|
|
$found_key = true; |
1072
|
|
|
} |
1073
|
|
|
|
1074
|
1 |
|
return $found_key; |
1075
|
|
|
} |
1076
|
|
|
|
1077
|
|
|
/** |
1078
|
|
|
* Searches the message body or the raw email in search of the key |
1079
|
|
|
* |
1080
|
|
|
* - Not found in the headers, so lets search the body for the [key] |
1081
|
|
|
* as we insert that on outbound email just for this |
1082
|
|
|
*/ |
1083
|
1 |
|
private function _load_key_from_body() |
1084
|
|
|
{ |
1085
|
1 |
|
$regex_key = '~\[(([a-z0-9]{32})\-(p|t|m)(\d+))\]~i'; |
1086
|
1 |
|
$found_key = false; |
1087
|
|
|
|
1088
|
|
|
// Check the message body |
1089
|
1 |
|
if (preg_match($regex_key, $this->body, $match) === 1) |
1090
|
|
|
{ |
1091
|
1 |
|
$this->headers['in-reply-to'] = $match[1]; |
1092
|
1 |
|
$this->_load_key_details($match); |
1093
|
1 |
|
$found_key = true; |
1094
|
|
|
} |
1095
|
|
|
// Grrr ... check everything! |
1096
|
|
View Code Duplication |
elseif (preg_match($regex_key, $this->raw_message, $match) === 1) |
1097
|
|
|
{ |
1098
|
|
|
$this->headers['in-reply-to'] = $match[1]; |
1099
|
|
|
$this->_load_key_details($match); |
1100
|
|
|
$found_key = true; |
1101
|
|
|
} |
1102
|
|
|
|
1103
|
1 |
|
return $found_key; |
1104
|
|
|
} |
1105
|
|
|
|
1106
|
|
|
/** |
1107
|
|
|
* Loads found key details for use in other functions |
1108
|
|
|
* |
1109
|
|
|
* @param string[] $match from regex 1=>full, 2=>key, 3=>p|t|m, 4=>12345 |
1110
|
|
|
*/ |
1111
|
1 |
|
private function _load_key_details($match) |
1112
|
|
|
{ |
1113
|
1 |
|
if (!empty($match[1])) |
1114
|
|
|
{ |
1115
|
|
|
// 1=>7738c27ae6c431495ad26587f30e2121-m29557, 2=>7738c27ae6c431495ad26587f30e2121, 3=>m, 4=>29557 |
1116
|
1 |
|
$this->message_key_id = $match[1]; |
1117
|
1 |
|
$this->message_key = $match[2]; |
1118
|
1 |
|
$this->message_type = $match[3]; |
1119
|
1 |
|
$this->message_id = (int) $match[4]; |
1120
|
|
|
} |
1121
|
1 |
|
} |
1122
|
|
|
|
1123
|
|
|
/** |
1124
|
|
|
* Loads in the most email from, to and cc address |
1125
|
|
|
* |
1126
|
|
|
* - will attempt to return the name and address for fields "name:" <email> |
1127
|
|
|
* - will become email['to'] = email and email['to_name'] = name |
1128
|
|
|
* |
1129
|
|
|
* @return array of addresses |
1130
|
|
|
*/ |
1131
|
1 |
|
public function load_address() |
1132
|
|
|
{ |
1133
|
1 |
|
$this->email['to'] = array(); |
1134
|
1 |
|
$this->email['from'] = array(); |
1135
|
1 |
|
$this->email['cc'] = array(); |
1136
|
|
|
|
1137
|
|
|
// Fetch the "From" email and if possibly the senders common name |
1138
|
1 |
|
if (isset($this->headers['from'])) |
1139
|
|
|
{ |
1140
|
1 |
|
$this->_parse_address($this->headers['from']); |
1141
|
1 |
|
$this->email['from'] = $this->_email_address; |
1142
|
1 |
|
$this->email['from_name'] = $this->_email_name; |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
// Fetch the "To" email and if possible the recipients common name |
1146
|
1 |
View Code Duplication |
if (isset($this->headers['to'])) |
1147
|
|
|
{ |
1148
|
1 |
|
$to_addresses = explode(',', $this->headers['to']); |
1149
|
1 |
|
for ($i = 0, $num = count($to_addresses); $i < $num; $i++) |
1150
|
|
|
{ |
1151
|
1 |
|
$this->_parse_address($to_addresses[$i]); |
1152
|
1 |
|
$this->email['to'][$i] = $this->_email_address; |
1153
|
1 |
|
$this->email['to_name'][$i] = $this->_email_name; |
1154
|
|
|
} |
1155
|
|
|
} |
1156
|
|
|
|
1157
|
|
|
// Fetch the "cc" address if there is one and once again the real name as well |
1158
|
1 |
View Code Duplication |
if (isset($this->headers['cc'])) |
1159
|
|
|
{ |
1160
|
|
|
$cc_addresses = explode(',', $this->headers['cc']); |
1161
|
|
|
for ($i = 0, $num = count($cc_addresses); $i < $num; $i++) |
1162
|
|
|
{ |
1163
|
|
|
$this->_parse_address($cc_addresses[$i]); |
1164
|
|
|
$this->email['cc'][$i] = $this->_email_address; |
1165
|
|
|
$this->email['cc_name'][$i] = $this->_email_name; |
1166
|
|
|
} |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
1 |
|
return $this->email; |
1170
|
|
|
} |
1171
|
|
|
|
1172
|
|
|
/** |
1173
|
|
|
* Finds the message sending ip and returns it |
1174
|
|
|
* |
1175
|
|
|
* - will look in various header fields where the ip may reside |
1176
|
|
|
* - returns false if it can't find a valid IP4 |
1177
|
|
|
* |
1178
|
|
|
* @return string|boolean on fail |
1179
|
|
|
*/ |
1180
|
1 |
|
public function load_ip() |
1181
|
|
|
{ |
1182
|
1 |
|
$this->ip = false; |
1183
|
|
|
|
1184
|
|
|
// The sending IP can be useful in spam prevention and making a post |
1185
|
1 |
|
if (isset($this->headers['x-posted-by'])) |
1186
|
|
|
{ |
1187
|
|
|
$this->ip = $this->_parse_ip($this->headers['x-posted-by']); |
1188
|
|
|
} |
1189
|
1 |
|
elseif (isset($this->headers['x-originating-ip'])) |
1190
|
|
|
{ |
1191
|
|
|
$this->ip = $this->_parse_ip($this->headers['x-originating-ip']); |
1192
|
|
|
} |
1193
|
1 |
|
elseif (isset($this->headers['x-senderip'])) |
1194
|
|
|
{ |
1195
|
|
|
$this->ip = $this->_parse_ip($this->headers['x-senderip']); |
1196
|
|
|
} |
1197
|
1 |
|
elseif (isset($this->headers['x-mdremoteip'])) |
1198
|
|
|
{ |
1199
|
|
|
$this->ip = $this->_parse_ip($this->headers['x-mdremoteip']); |
1200
|
|
|
} |
1201
|
1 |
|
elseif (isset($this->headers['received'])) |
1202
|
|
|
{ |
1203
|
1 |
|
$this->ip = $this->_parse_ip($this->headers['received']); |
1204
|
|
|
} |
1205
|
|
|
|
1206
|
1 |
|
return $this->ip; |
1207
|
|
|
} |
1208
|
|
|
|
1209
|
|
|
/** |
1210
|
|
|
* Finds if any spam headers have been positively set and returns that flag |
1211
|
|
|
* |
1212
|
|
|
* - will look in various header fields where the spam status may reside |
1213
|
|
|
* |
1214
|
|
|
* @return boolean on fail |
1215
|
|
|
*/ |
1216
|
1 |
|
public function load_spam() |
1217
|
|
|
{ |
1218
|
|
|
// SpamAssassin (and others like rspamd) |
1219
|
1 |
|
if (isset($this->headers['x-spam-flag']) && strtolower(substr($this->headers['x-spam-flag'], 0, 3)) === 'yes') |
1220
|
|
|
{ |
1221
|
1 |
|
$this->spam_found = true; |
1222
|
|
|
} |
1223
|
|
|
// SpamStopper and other variants |
1224
|
|
View Code Duplication |
elseif (isset($this->headers['x-spam-status']) && strtolower(substr($this->headers['x-spam-status'], 0, 3)) === 'yes') |
1225
|
|
|
{ |
1226
|
|
|
$this->spam_found = true; |
1227
|
|
|
} |
1228
|
|
|
// j-chkmail -- hi = likely spam lo = suspect ... |
1229
|
|
View Code Duplication |
elseif (isset($this->headers['x-j-chkmail-status']) && strtolower(substr($this->headers['x-j-chkmail-status'], 0, 2)) === 'hi') |
1230
|
|
|
{ |
1231
|
|
|
$this->spam_found = true; |
1232
|
|
|
} |
1233
|
|
|
// Nucleus Mailscanner |
1234
|
|
View Code Duplication |
elseif (isset($this->headers['x-nucleus-mailscanner']) && strtolower($this->headers['x-nucleus-mailscanner']) !== 'found to be clean') |
1235
|
|
|
{ |
1236
|
|
|
$this->spam_found = true; |
1237
|
|
|
} |
1238
|
|
|
|
1239
|
1 |
|
return $this->spam_found; |
1240
|
|
|
} |
1241
|
|
|
|
1242
|
|
|
/** |
1243
|
|
|
* Validates that the ip is a valid ip4 address |
1244
|
|
|
* |
1245
|
|
|
* @param string|null $string |
1246
|
|
|
* @return string |
1247
|
|
|
*/ |
1248
|
1 |
|
private function _parse_ip($string) |
1249
|
|
|
{ |
1250
|
1 |
|
if (preg_match('~\[?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]?~', $string, $matches) !== 1) |
1251
|
|
|
{ |
1252
|
|
|
return ''; |
1253
|
|
|
} |
1254
|
|
|
|
1255
|
1 |
|
$string = trim($matches[0], '[] '); |
1256
|
|
|
|
1257
|
|
|
// Validate it matches an ip4 standard |
1258
|
1 |
|
if (filter_var($string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) |
1259
|
|
|
{ |
1260
|
1 |
|
return $string; |
1261
|
|
|
} |
1262
|
|
|
else |
1263
|
|
|
{ |
1264
|
|
|
return ''; |
1265
|
|
|
} |
1266
|
|
|
} |
1267
|
|
|
|
1268
|
|
|
/** |
1269
|
|
|
* Take an email address and parse out the email address and email name |
1270
|
|
|
* |
1271
|
|
|
* @param string $val |
1272
|
|
|
*/ |
1273
|
1 |
|
private function _parse_address($val) |
1274
|
|
|
{ |
1275
|
1 |
|
$this->_email_address = ''; |
1276
|
1 |
|
$this->_email_name = ''; |
1277
|
|
|
|
1278
|
1 |
|
if (preg_match('~(.*?)<(.*?)>~', $val, $matches)) |
1279
|
|
|
{ |
1280
|
|
|
// The email address, remove spaces and (comments) |
1281
|
1 |
|
$this->_email_address = trim(str_replace(' ', '', $matches[2])); |
1282
|
1 |
|
$this->_email_address = preg_replace('~\(.*?\)~', '', $this->_email_address); |
1283
|
|
|
|
1284
|
|
|
// Perhaps a common name as well "name:" <email> |
1285
|
1 |
|
if (!empty($matches[1])) |
1286
|
|
|
{ |
1287
|
1 |
|
$matches[1] = $this->_decode_header($matches[1]); |
1288
|
1 |
|
if ($matches[1][0] === '"' && substr($matches[1], -1) === '"') |
1289
|
|
|
{ |
1290
|
1 |
|
$this->_email_name = substr($matches[1], 1, -1); |
1291
|
|
|
} |
1292
|
|
|
else |
1293
|
|
|
{ |
1294
|
1 |
|
$this->_email_name = $matches[1]; |
1295
|
|
|
} |
1296
|
|
|
} |
1297
|
|
|
else |
1298
|
|
|
{ |
1299
|
1 |
|
$this->_email_name = $this->_email_address; |
1300
|
|
|
} |
1301
|
|
|
|
1302
|
|
|
// Check the validity of the common name, if not sure set it to email user. |
1303
|
1 |
|
if (!preg_match('~^\w+~', $this->_email_name)) |
1304
|
|
|
{ |
1305
|
1 |
|
$this->_email_name = substr($this->_email_address, 0, strpos($this->_email_address, '@')); |
1306
|
|
|
} |
1307
|
|
|
} |
1308
|
|
|
else |
1309
|
|
|
{ |
1310
|
|
|
// Just an sad lonely email address, so we use it as is |
1311
|
|
|
$this->_email_address = trim(str_replace(' ', '', $val)); |
1312
|
|
|
$this->_email_address = preg_replace('~\(.*?\)~', '', $this->_email_address); |
1313
|
|
|
$this->_email_name = substr($this->_email_address, 0, strpos($this->_email_address, '@')); |
1314
|
|
|
} |
1315
|
1 |
|
} |
1316
|
|
|
|
1317
|
|
|
/** |
1318
|
|
|
* Decodes base64 or quoted-printable strings |
1319
|
|
|
* Converts from one character set to utf-8 |
1320
|
|
|
* |
1321
|
|
|
* @param string $string |
1322
|
|
|
* @param string $encoding |
1323
|
|
|
* @param string $charset |
1324
|
|
|
* |
1325
|
|
|
* @return bool|null|string|string[] |
1326
|
|
|
*/ |
1327
|
1 |
|
private function _decode_string($string, $encoding, $charset = '') |
1328
|
|
|
{ |
1329
|
|
|
// Decode if its quoted printable or base64 encoded |
1330
|
1 |
|
if ($encoding === 'quoted-printable') |
1331
|
|
|
{ |
1332
|
|
|
$string = quoted_printable_decode(preg_replace('~=\r?\n~', '', $string)); |
1333
|
|
|
} |
1334
|
1 |
|
elseif ($encoding === 'base64') |
1335
|
|
|
{ |
1336
|
|
|
$string = base64_decode($string); |
1337
|
|
|
} |
1338
|
|
|
|
1339
|
|
|
// Convert this to utf-8 if needed. |
1340
|
1 |
|
if (!empty($charset) && $charset !== 'UTF-8') |
1341
|
|
|
{ |
1342
|
|
|
$string = $this->_charset_convert($string, strtoupper($charset), 'UTF-8'); |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
1 |
|
return $string; |
1346
|
|
|
} |
1347
|
|
|
|
1348
|
|
|
/** |
1349
|
|
|
* Pick the best possible function to convert a strings character set, if any exist |
1350
|
|
|
* |
1351
|
|
|
* @param string $string |
1352
|
|
|
* @param string $from |
1353
|
|
|
* @param string $to |
1354
|
|
|
* |
1355
|
|
|
* @return null|string|string[] |
1356
|
|
|
*/ |
1357
|
|
|
private function _charset_convert($string, $from, $to) |
1358
|
|
|
{ |
1359
|
|
|
// Lets assume we have one of the functions available to us |
1360
|
|
|
$this->_converted_utf8 = true; |
1361
|
|
|
$string_save = $string; |
1362
|
|
|
|
1363
|
|
|
// Use iconv if its available |
1364
|
|
|
if (function_exists('iconv')) |
1365
|
|
|
{ |
1366
|
|
|
$string = @iconv($from, $to . '//TRANSLIT//IGNORE', $string); |
1367
|
|
|
} |
1368
|
|
|
|
1369
|
|
|
// No iconv or a false response from it |
1370
|
|
|
if (!function_exists('iconv') || ($string === false)) |
1371
|
|
|
{ |
1372
|
|
|
if (function_exists('mb_convert_encoding')) |
1373
|
|
|
{ |
1374
|
|
|
// Replace unknown characters with a space |
1375
|
|
|
@ini_set('mbstring.substitute_character', '32'); |
1376
|
|
|
$string = @mb_convert_encoding($string, $to, $from); |
1377
|
|
|
} |
1378
|
|
|
elseif (function_exists('recode_string')) |
1379
|
|
|
{ |
1380
|
|
|
$string = @recode_string($from . '..' . $to, $string); |
1381
|
|
|
} |
1382
|
|
|
else |
1383
|
|
|
{ |
1384
|
|
|
$this->_converted_utf8 = false; |
1385
|
|
|
} |
1386
|
|
|
} |
1387
|
|
|
|
1388
|
|
|
unset($string_save); |
1389
|
|
|
|
1390
|
|
|
return $string; |
1391
|
|
|
} |
1392
|
|
|
} |
1393
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.