[TASK] Remove XCLASS definitions from cache classes
[Packages/TYPO3.CMS.git] / t3lib / class.t3lib_readmail.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 1999-2011 Kasper Skårhøj (kasperYYYY@typo3.com)
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 * A copy is found in the textfile GPL.txt and important notices to the license
17 * from the author is found in LICENSE.txt distributed with these scripts.
18 *
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27 /**
28 * Contains a class with functions used to read email content
29 *
30 * Revised for TYPO3 3.6 May 2003 by Kasper Skårhøj
31 *
32 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
33 */
34
35
36 /**
37 * Functions used to read email content
38 * The class is still just a bunch of miscellaneous functions used to read content out of emails
39 *
40 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
41 * @package TYPO3
42 * @subpackage t3lib
43 */
44 class t3lib_readmail {
45 var $dateAbbrevs = array(
46 'JAN' => 1,
47 'FEB' => 2,
48 'MAR' => 3,
49 'APR' => 4,
50 'MAY' => 5,
51 'JUN' => 6,
52 'JUL' => 7,
53 'AUG' => 8,
54 'SEP' => 9,
55 'OCT' => 10,
56 'NOV' => 11,
57 'DEC' => 12
58 );
59 var $serverGMToffsetMinutes = 60; // = +0100 (CET)
60
61 /*******************************
62 *
63 * General
64 *
65 ********************************/
66
67 /**
68 * Returns the text content of a mail which has previously been parsed by eg. extractMailHeader()
69 * Probably obsolete since the function fullParse() is more advanced and safer to use.
70 *
71 * @param array Output from extractMailHeader()
72 * @return string The content.
73 */
74 function getMessage($mailParts) {
75 if ($mailParts['content-type']) {
76 $CType = $this->getCType($mailParts['content-type']);
77 if ($CType['boundary']) {
78 $parts = $this->getMailBoundaryParts($CType['boundary'], $mailParts['CONTENT']);
79 $c = $this->getTextContent($parts[0]);
80 } else {
81 $c = $this->getTextContent(
82 'Content-Type: ' . $mailParts['content-type'] . '
83 ' . $mailParts['CONTENT']
84 );
85 }
86 } else {
87 $c = $mailParts['CONTENT'];
88 }
89
90 return $c;
91 }
92
93 /**
94 * Returns the body part of a raw mail message (including headers)
95 * Probably obsolete since the function fullParse() is more advanced and safer to use.
96 *
97 * @param string Raw mail content
98 * @return string Body of message
99 */
100 function getTextContent($content) {
101 $p = $this->extractMailHeader($content);
102
103 // Here some decoding might be needed...
104 // However we just return what is believed to be the proper notification:
105 return $p['CONTENT'];
106 }
107
108 /**
109 * Splits the body of a mail into parts based on the boundary string given.
110 * Obsolete, use fullParse()
111 *
112 * @param string Boundary string used to split the content.
113 * @param string BODY section of a mail
114 * @return array Parts of the mail based on this
115 */
116 function getMailBoundaryParts($boundary, $content) {
117 $mParts = explode('--' . $boundary, $content);
118 unset($mParts[0]);
119 $new = array();
120 foreach ($mParts as $val) {
121 if (trim($val) == '--') {
122 break;
123 }
124 $new[] = ltrim($val);
125 }
126
127 return $new;
128 }
129
130 /**
131 * Returns Content Type plus more.
132 * Obsolete, use fullParse()
133 *
134 * @param string "ContentType" string with more
135 * @return array parts in key/value pairs
136 * @ignore
137 */
138 function getCType($str) {
139 $parts = explode(';', $str);
140 $cTypes = array();
141 $cTypes['ContentType'] = $parts[0];
142 next($parts);
143 while (list(, $ppstr) = each($parts)) {
144 $mparts = explode('=', $ppstr, 2);
145 if (count($mparts) > 1) {
146 $cTypes[strtolower(trim($mparts[0]))] = preg_replace('/^"/', '', trim(preg_replace('/"$/', '', trim($mparts[1]))));
147 } else {
148 $cTypes[] = $ppstr;
149 }
150 }
151
152 return $cTypes;
153 }
154
155 /**
156 * Analyses the return-mail content for the Dmailer module - used to find what reason there was for rejecting the mail
157 * Used by the Dmailer, but not exclusively.
158 *
159 * @param string message body/text
160 * @return array key/value pairs with analysis result. Eg. "reason", "content", "reason_text", "mailserver" etc.
161 */
162 function analyseReturnError($c) {
163 $cp = array();
164 if (strstr($c, '--- Below this line is a copy of the message.')) { // QMAIL
165 list($c) = explode('--- Below this line is a copy of the message.', $c); // Splits by the QMAIL divider
166 $cp['content'] = trim($c);
167 $parts = explode('>:', $c, 2);
168 $cp['reason_text'] = trim($parts[1]);
169 $cp['mailserver'] = 'Qmail';
170 if (preg_match('/550|no mailbox|account does not exist/i', $cp['reason_text'])) {
171 $cp['reason'] = 550; // 550 Invalid recipient
172 } elseif (stristr($cp['reason_text'], 'couldn\'t find any host named')) {
173 $cp['reason'] = 2; // Bad host
174 } elseif (preg_match('/Error in Header|invalid Message-ID header/i', $cp['reason_text'])) {
175 $cp['reason'] = 554;
176 } else {
177 $cp['reason'] = -1;
178 }
179 } elseif (strstr($c, 'The Postfix program')) { // Postfix
180 $cp['content'] = trim($c);
181 $parts = explode('>:', $c, 2);
182 $cp['reason_text'] = trim($parts[1]);
183 $cp['mailserver'] = 'Postfix';
184 if (stristr($cp['reason_text'], '550')) {
185 $cp['reason'] = 550; // 550 Invalid recipient, User unknown
186 } elseif (stristr($cp['reason_text'], '553')) {
187 $cp['reason'] = 553; // No such user
188 } elseif (stristr($cp['reason_text'], '551')) {
189 $cp['reason'] = 551; // Mailbox full
190 } else {
191 $cp['reason'] = -1;
192 }
193 } else { // No-named:
194 $cp['content'] = trim($c);
195 $cp['reason_text'] = trim(substr($c, 0, 1000));
196 $cp['mailserver'] = 'unknown';
197 if (preg_match('/Unknown Recipient|Delivery failed 550|Receiver not found|User not listed|recipient problem|Delivery to the following recipients failed|User unknown|recipient name is not recognized/i', $cp['reason_text'])) {
198 $cp['reason'] = 550; // 550 Invalid recipient, User unknown
199 } elseif (preg_match('/over quota|mailbox full/i', $cp['reason_text'])) {
200 $cp['reason'] = 551;
201 } elseif (preg_match('/Error in Header/i', $cp['reason_text'])) {
202 $cp['reason'] = 554;
203 } else {
204 $cp['reason'] = -1;
205 }
206 }
207
208 return $cp;
209 }
210
211 /**
212 * Decodes a header-string with the =?....?= syntax including base64/quoted-printable encoding.
213 *
214 * @param string A string (encoded or not) from a mail header, like sender name etc.
215 * @return string The input string, but with the parts in =?....?= decoded.
216 */
217 function decodeHeaderString($str) {
218 $parts = explode('=?', $str, 2);
219 if (count($parts) == 2) {
220 list($charset, $encType, $encContent) = explode('?', $parts[1], 3);
221 $subparts = explode('?=', $encContent, 2);
222 $encContent = $subparts[0];
223
224 switch (strtolower($encType)) {
225 case 'q':
226 $encContent = quoted_printable_decode($encContent);
227 $encContent = str_replace('_', ' ', $encContent);
228 break;
229 case 'b':
230 $encContent = base64_decode($encContent);
231 break;
232 }
233
234 $parts[1] = $encContent . $this->decodeHeaderString($subparts[1]); // Calls decodeHeaderString recursively for any subsequent encoded section.
235 }
236
237 return implode('', $parts);
238 }
239
240 /**
241 * Extracts name/email parts from a header field (like 'To:' or 'From:' with name/email mixed up.
242 *
243 * @param string Value from a header field containing name/email values.
244 * @return array Array with the name and email in. Email is validated, otherwise not set.
245 */
246 function extractNameEmail($str) {
247 $outArr = array();
248
249 // Email:
250 $reg = '';
251 preg_match('/<([^>]*)>/', $str, $reg);
252 if (t3lib_div::validEmail($str)) {
253 $outArr['email'] = $str;
254 } elseif ($reg[1] && t3lib_div::validEmail($reg[1])) {
255 $outArr['email'] = $reg[1];
256 // Find name:
257 list($namePart) = explode($reg[0], $str);
258 if (trim($namePart)) {
259 $reg = '';
260 preg_match('/"([^"]*)"/', $str, $reg);
261 if (trim($reg[1])) {
262 $outArr['name'] = trim($reg[1]);
263 } else {
264 $outArr['name'] = trim($namePart);
265 }
266 }
267 }
268
269 return $outArr;
270 }
271
272 /**
273 * Returns the data from the 'content-type' field. That is the boundary, charset and mime-type
274 *
275 * @param string "Content-type-string"
276 * @return array key/value pairs with the result.
277 */
278 function getContentTypeData($contentTypeStr) {
279 $outValue = array();
280 $cTypeParts = t3lib_div::trimExplode(';', $contentTypeStr, 1);
281 $outValue['_MIME_TYPE'] = $cTypeParts[0]; // content type, first value is supposed to be the mime-type, whatever after the first is something else.
282
283 reset($cTypeParts);
284 next($cTypeParts);
285 while (list(, $v) = Each($cTypeParts)) {
286 $reg = '';
287 preg_match('/([^=]*)="(.*)"/i', $v, $reg);
288 if (trim($reg[1]) && trim($reg[2])) {
289 $outValue[strtolower($reg[1])] = $reg[2];
290 }
291 }
292
293 return $outValue;
294 }
295
296 /**
297 * Makes a UNIX-date based on the timestamp in the 'Date' header field.
298 *
299 * @param string String with a timestamp according to email standards.
300 * @return integer The timestamp converted to unix-time in seconds and compensated for GMT/CET ($this->serverGMToffsetMinutes);
301 */
302 function makeUnixDate($dateStr) {
303 $dateParts = explode(',', $dateStr);
304 $dateStr = count($dateParts) > 1 ? $dateParts[1] : $dateParts[0];
305
306 $spaceParts = t3lib_div::trimExplode(' ', $dateStr, 1);
307
308 $spaceParts[1] = $this->dateAbbrevs[strtoupper($spaceParts[1])];
309 $timeParts = explode(':', $spaceParts[3]);
310 $timeStamp = mktime($timeParts[0], $timeParts[1], $timeParts[2], $spaceParts[1], $spaceParts[0], $spaceParts[2]);
311
312 $offset = $this->getGMToffset($spaceParts[4]);
313 $timeStamp -= ($offset * 60); // Compensates for GMT by subtracting the number of seconds which the date is offset from serverTime
314
315 return $timeStamp;
316 }
317
318 /**
319 * Parsing the GMT offset value from a mail timestamp.
320 *
321 * @param string A string like "+0100" or so.
322 * @return integer Minutes to offset the timestamp
323 * @access private
324 */
325 function getGMToffset($GMT) {
326 $GMToffset = substr($GMT, 1, 2) * 60 + substr($GMT, 3, 2);
327 $GMToffset *= substr($GMT, 0, 1) == '+' ? 1 : -1;
328 $GMToffset -= $this->serverGMToffsetMinutes;
329
330 return $GMToffset;
331 }
332
333 /**
334 * This returns the mail header items in an array with associative keys and the mail body part in another CONTENT field
335 *
336 * @param string Raw mail content
337 * @param integer A safety limit that will put a upper length to how many header chars will be processed. Set to zero means that there is no limit. (Uses a simple substr() to limit the amount of mail data to process to avoid run-away)
338 * @return array An array where each key/value pair is a header-key/value pair. The mail BODY is returned in the key 'CONTENT' if $limit is not set!
339 */
340 function extractMailHeader($content, $limit = 0) {
341 if ($limit) {
342 $content = substr($content, 0, $limit);
343 }
344
345 $lines = explode(LF, ltrim($content));
346 $headers = array();
347 $p = '';
348 foreach ($lines as $k => $str) {
349 if (!trim($str)) {
350 break;
351 } // header finished
352 $parts = explode(' ', $str, 2);
353 if ($parts[0] && substr($parts[0], -1) == ':') {
354 $p = strtolower(substr($parts[0], 0, -1));
355 if (isset($headers[$p])) {
356 $headers[$p . '.'][] = $headers[$p];
357 $headers[$p] = '';
358 }
359 $headers[$p] = trim($parts[1]);
360 } else {
361 $headers[$p] .= ' ' . trim($str);
362 }
363 unset($lines[$k]);
364 }
365 if (!$limit) {
366 $headers['CONTENT'] = ltrim(implode(LF, $lines));
367 }
368
369 return $headers;
370 }
371
372 /**
373 * The extended version of the extractMailHeader() which will also parse all the content body into an array and further process the header fields and decode content etc. Returns every part of the mail ready to go.
374 *
375 * @param string Raw email input.
376 * @return array Multidimensional array with all parts of the message organized nicely. Use t3lib_utility_Debug::debug() to analyse it visually.
377 */
378 function fullParse($content) {
379 // *************************
380 // PROCESSING the HEADER part of the mail
381 // *************************
382
383 // Splitting header and body of mail:
384 $mailParts = $this->extractMailHeader($content);
385
386 // Decoding header values which potentially can be encoded by =?...?=
387 $list = explode(',', 'subject,thread-topic,from,to');
388 foreach ($list as $headerType) {
389 if (isset($mailParts[$headerType])) {
390 $mailParts[$headerType] = $this->decodeHeaderString($mailParts[$headerType]);
391 }
392 }
393 // Separating email/names from header fields which can contain email addresses.
394 $list = explode(',', 'from,to,reply-to,sender,return-path');
395 foreach ($list as $headerType) {
396 if (isset($mailParts[$headerType])) {
397 $mailParts['_' . strtoupper($headerType)] = $this->extractNameEmail($mailParts[$headerType]);
398 }
399 }
400 // Decode date from human-readable format to unix-time (includes compensation for GMT CET)
401 $mailParts['_DATE'] = $this->makeUnixDate($mailParts['date']);
402
403 // Transfer encodings of body content
404 switch (strtolower($mailParts['content-transfer-encoding'])) {
405 case 'quoted-printable':
406 $mailParts['CONTENT'] = quoted_printable_decode($mailParts['CONTENT']);
407 break;
408 case 'base64':
409 $mailParts['CONTENT'] = base64_decode($mailParts['CONTENT']);
410 break;
411 }
412
413 // Content types
414 $mailParts['_CONTENT_TYPE_DAT'] = $this->getContentTypeData($mailParts['content-type']);
415
416
417 // *************************
418 // PROCESSING the CONTENT part of the mail (the body)
419 // *************************
420
421 $cType = strtolower($mailParts['_CONTENT_TYPE_DAT']['_MIME_TYPE']);
422 $cType = substr($cType, 0, 9); // only looking for 'multipart' in string.
423 switch ($cType) {
424 case 'multipart':
425 if ($mailParts['_CONTENT_TYPE_DAT']['boundary']) {
426 $contentSectionParts = t3lib_div::trimExplode('--' . $mailParts['_CONTENT_TYPE_DAT']['boundary'], $mailParts['CONTENT'], 1);
427 $contentSectionParts_proc = array();
428
429 foreach ($contentSectionParts as $k => $v) {
430 if (substr($v, 0, 2) == '--') {
431 break;
432 }
433 $contentSectionParts_proc[$k] = $this->fullParse($v);
434 }
435 $mailParts['CONTENT'] = $contentSectionParts_proc;
436 } else {
437 $mailParts['CONTENT'] = 'ERROR: No boundary found.';
438 }
439 break;
440 default:
441 if (strtolower($mailParts['_CONTENT_TYPE_DAT']['charset']) == 'utf-8') {
442 $mailParts['CONTENT'] = utf8_decode($mailParts['CONTENT']);
443 }
444 break;
445 }
446
447 return $mailParts;
448 }
449 }
450
451 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_readmail.php'])) {
452 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_readmail.php']);
453 }
454 ?>