Reduce mbox-helper overhead and fix some bugs
[banana.git] / banana / mimepart.inc.php
CommitLineData
7027794f 1<?php
2/********************************************************************************
3* banana/mimepart.inc.php : class for MIME parts
4* ------------------------
5*
6* This file is part of the banana distribution
7* Copyright: See COPYING files that comes with this distribution
8********************************************************************************/
9
10class BananaMimePart
11{
12 public $headers = null; /* Should be protected */
13
14 private $id = null;
15 private $content_type = null;
16 private $charset = null;
17 private $encoding = null;
18 private $disposition = null;
19 private $boundary = null;
20 private $filename = null;
21 private $format = null;
22
23 private $body = null;
24 private $multipart = null;
25
26 protected function __construct($data = null)
27 {
28 if (!is_null($data)) {
29 $this->fromRaw($data);
30 }
31 }
32
33 protected function makeTextPart($body, $content_type, $encoding, $charset = null, $format = 'fixed')
34 {
35 $this->body = $body;
36 $this->charset = $charset;
37 $this->encoding = $encoding;
38 $this->content_type = $content_type;
39 $this->format = strtolower($format);
40 $this->parse();
41 }
42
43 protected function makeDataPart($body, $content_type, $encoding, $filename, $disposition, $id = null)
44 {
45 $this->body = $body;
46 $this->content_type = $content_type;
47 $this->encoding = $encoding;
48 $this->filename = $filename;
49 $this->disposition = $disposition;
50 $this->id = $id;
51 if (is_null($content_type) || $content_type == 'application/octet-stream') {
52 $this->decodeContent();
53 $this->content_type = BananaMimePart::getMimeType($body, false);
54 }
55 }
56
57 protected function makeFilePart($file, $content_type =null, $disposition = 'attachment')
58 {
59 $body = file_get_contents($file['tmp_name']);
60 if ($body === false || strlen($body) != $file['size']) {
61 return false;
62 }
63 if (is_null($content_type) || $content_type == 'application/octet-stream') {
64 $content_type = BananaMimePart::getMimeType($file['tmp_name']);
65 }
66 if (substr($content_type, 0, 5) == 'text/') {
67 $encoding = '8bit';
68 } else {
69 $encoding = 'base64';
70 $body = chunk_split(base64_encode($body));
71 }
72 $this->filename = $file['name'];
73 $this->content_type = $content_type;
74 $this->disposition = $disposition;
75 $this->body = $body;
76 $this->encoding = $encoding;
77 return true;
78 }
79
80 protected function makeMultiPart($body, $content_type, $encoding, $boundary)
81 {
82 $this->body = $body;
83 $this->content_type = $content_type;
84 $this->encoding = $encoding;
85 $this->boundary = $boundary;
86 $this->parse();
87 }
88
89 protected function convertToMultiPart()
90 {
91 if (!$this->isType('multipart', 'mixed')) {
92 $newpart = $this;
93 $this->content_type = 'multipart/mixed';
94 $this->encoding = '8bit';
95 $this->multipart = array($newpart);
96 $this->headers = null;
97 $this->charset = null;
98 $this->disposition = null;
99 $this->filename = null;
100 $this->boundary = null;
101 $this->body = null;
102 $this->format = null;
103 $this->id = null;
104 }
105 }
106
107 public function addAttachment(array $file, $content_type = null, $disposition = 'attachment')
108 {
109 $newpart = new BananaMimePart;
110 if (!is_uploaded_file($file['tmp_name'])) {
111 return false;
112 }
113 if ($newpart->makeFilePart($file, $content_type, $disposition)) {
114 $this->convertToMultiPart();
115 $this->multipart[] = $newpart;
116 return true;
117 }
118 return false;
119 }
120
121 protected function getHeader($title, $filter = null)
122 {
123 if (!isset($this->headers[$title])) {
124 return null;
125 }
126 $header =& $this->headers[$title];
127 if (is_null($filter)) {
128 return trim($header);
129 } elseif (preg_match($filter, $header, $matches)) {
130 return trim($matches[1]);
131 }
132 return null;
133 }
134
135 protected function fromRaw($data)
136 {
137 if (is_array($data)) {
138 if (array_key_exists('From', $data)) {
139 $this->headers = array_map(array($this, 'decodeHeader'), array_change_key_case($data));
140 return;
141 } else {
142 $lines = $data;
143 }
144 } else {
145 $lines = explode("\n", $data);
146 }
147 $headers = BananaMimePart::parseHeaders($lines);
148 $this->headers =& $headers;
149 if (empty($headers) || empty($lines)) {
150 return;
151 }
152 $content = join("\n", $lines);
153 $test = trim($content);
154 if (empty($test)) {
155 return;
156 }
157
158 $content_type = strtolower($this->getHeader('content-type', '/^\s*([^ ;]+?)(;|$)/'));
159 if (empty($content_type)) {
160 $encoding = '8bit';
161 $charset = 'CP1252';
162 $content_type = 'text/plain';
19fc7e1d 163 $format = strtolower($this->getHeader('x-rfc2646', '/format="?([^ w@"]+?)"?\s*(;|$)/i'));
7027794f 164 } else {
165 $encoding = strtolower($this->getHeader('content-transfer-encoding'));
166 $disposition = $this->getHeader('content-disposition', '/(inline|attachment)/i');
19fc7e1d 167 $boundary = $this->getHeader('content-type', '/boundary="?([^ "]+?)"?\s*(;|$)/i');
168 $charset = strtolower($this->getHeader('content-type', '/charset="?([^ "]+?)"?\s*(;|$)/i'));
169 $filename = $this->getHeader('content-disposition', '/filename="?([^ "]+?)"?\s*(;|$)/i');
170 $format = strtolower($this->getHeader('content-type', '/format="?([^ "]+?)"?\s*(;|$)/i'));
7027794f 171 $id = $this->getHeader('content-id', '/<(.*?)>/');
172 if (empty($filename)) {
173 $filename = $this->getHeader('content-type', '/name="?([^"]+)"?/');
174 }
175 }
176 list($type, $subtype) = explode('/', $content_type);
177 switch ($type) {
178 case 'text': case 'message':
179 $this->makeTextPart($content, $content_type, $encoding, $charset, $format);
180 break;
181 case 'multipart':
182 $this->makeMultiPart($content, $content_type, $encoding, $boundary);
183 break;
184 default:
185 $this->makeDataPart($content, $content_type, $encoding, $filename, $disposition, $id);
186 }
187 }
188
189 private function parse()
190 {
191 if ($this->isType('multipart')) {
192 $this->splitMultipart();
193 } else {
194 $parts = $this->findUUEncoded();
195 if (count($parts)) {
196 $this->convertToMultiPart();
197 $this->multipart = array_merge(array($textpart), $parts);
198 }
199 }
200 }
201
202 private function splitMultipart()
203 {
204 $this->decodeContent();
205 if (is_null($this->multipart)) {
206 $this->multipart = array();
207 }
208 $boundary =& $this->boundary;
209 $parts = preg_split("/\n--" . preg_quote($boundary, '/') . "(--|\n)/", $this->body, -1, PREG_SPLIT_NO_EMPTY);
210 foreach ($parts as &$part) {
211 $newpart = new BananaMimePart($part);
212 if (!is_null($newpart->content_type)) {
213 $this->multipart[] = $newpart;
214 }
215 }
216 $this->body = null;
217 }
218
219 public static function getMimeType($data, $is_filename = true)
220 {
221 if ($is_filename) {
222 $type = mime_content_type($arg);
223 } else {
224 $arg = escapeshellarg($data);
225 $type = preg_replace('/;.*/', '', trim(shell_exec("echo $arg | file -bi -")));
226 }
227 return empty($type) ? 'application/octet-stream' : $type;
228 }
229
230 private function findUUEncoded()
231 {
232 $this->decodeContent();
233 $parts = array();
234 if (preg_match_all("/\n(begin \d+ ([^\r\n]+)\r?(?:\n(?!end)[^\n]*)*\nend)/",
235 $this->body, $matches, PREG_SET_ORDER)) {
236 foreach ($matches as &$match) {
237 $data = convert_uudecode($match[1]);
238 $mime = BananaMimePart::getMimeType($data, false);
239 if ($mime != 'application/x-empty') {
240 $this->body = trim(str_replace($match[0], '', $this->body));
241 $newpart = new BananaMimePart;
242 $newpart->makeDataPart($data, $mime, '8bit', $match[2], 'attachment');
243 $parts[] = $newpart;
244 }
245 }
246 }
247 return $parts;
248 }
249
250 static private function _decodeHeader($charset, $c, $str)
251 {
252 $s = ($c == 'Q' || $c == 'q') ? quoted_printable_decode($str) : base64_decode($str);
253 $s = @iconv($charset, 'UTF-8', $s);
254 return str_replace('_', ' ', $s);
255 }
256
257 static public function decodeHeader(&$val, $key)
258 {
259 if (preg_match('/[\x80-\xff]/', $val)) {
260 if (!is_utf8($val)) {
261 $val = utf8_encode($val);
262 }
263 } else {
264 $val = preg_replace('/(=\?.*?\?[bq]\?.*?\?=) (=\?.*?\?[bq]\?.*?\?=)/i', '\1\2', $val);
265 $val = preg_replace('/=\?(.*?)\?([bq])\?(.*?)\?=/ie', 'BananaMimePart::_decodeHeader("\1", "\2", "\3")', $val);
266 }
267 }
268
269 static public function &parseHeaders(array &$lines)
270 {
271 $headers = array();
19fc7e1d 272 while ($lines) {
7027794f 273 $line = array_shift($lines);
19fc7e1d 274 if (isset($hdr) && $line && ctype_space($line{0})) {
7027794f 275 $headers[$hdr] .= ' ' . trim($line);
276 } elseif (!empty($line)) {
19fc7e1d 277 if (strpos($line, ':') !== false) {
278 list($hdr, $val) = explode(":", $line, 2);
7027794f 279 $hdr = strtolower($hdr);
e9360b11 280 if (in_array($hdr, Banana::$msgparse_headers)) {
19fc7e1d 281 $headers[$hdr] = ltrim($val);
7027794f 282 } else {
283 unset($hdr);
284 }
285 }
286 } else {
287 break;
288 }
289 }
290 array_walk($headers, array('BananaMimePart', 'decodeHeader'));
291 return $headers;
292 }
293
294 static public function encodeHeader($value, $trim = 0)
295 {
296 if ($trim) {
297 if (strlen($value) > $trim) {
298 $value = substr($value, 0, $trim);
299 }
300 }
7995f07b 301 $value = preg_replace('/([\x80-\xff]+)/e', '"=?UTF-8?B?" . base64_encode("\1") . "?="', $value);
7027794f 302 return $value;
303 }
304
305 private function decodeContent()
306 {
307 $encodings = Array('quoted-printable' => 'quoted_printable_decode',
308 'base64' => 'base64_decode',
309 'x-uuencode' => 'convert_uudecode');
310 foreach ($encodings as $encoding => $callback) {
311 if ($this->encoding == $encoding) {
312 $this->body = $callback($this->body);
313 $this->encoding = '8bit';
314 break;
315 }
316 }
317 if (!$this->isType('text')) {
318 return;
319 }
320
321 if (!is_null($this->charset)) {
322 $body = iconv($this->charset, 'UTF-8//IGNORE', $this->body);
323 if (empty($body)) {
324 return;
325 }
326 $this->body = $body;
327 } else {
328 $this->body = utf8_encode($this->body);
329 }
330 $this->charset = 'utf-8';
331 }
332
333 public function send($force_inline = false)
334 {
335 $this->decodeContent();
336 if ($force_inline) {
337 $dispostion = $this->disposition;
338 $this->disposition = 'inline';
339 }
340 $headers = $this->getHeaders();
341 foreach ($headers as $key => $value) {
342 header("$key: $value");
343 }
344 if ($force_inline) {
345 $this->disposition = $disposition;
346 }
347 echo $this->body;
348 exit;
349 }
350
351 private function setBoundary()
352 {
353 if ($this->isType('multipart') && is_null($this->boundary)) {
354 $this->boundary = '--banana-bound-' . time() . rand(0, 255) . '-';
355 }
356 }
357
358 public function getHeaders()
359 {
360 $headers = array();
361 $this->setBoundary();
362 $headers['Content-Type'] = $this->content_type . ";"
363 . ($this->filename ? " name=\"{$this->filename}\";" : '')
364 . ($this->charset ? " charset=\"{$this->charset}\";" : '')
365 . ($this->boundary ? " boundary=\"{$this->boundary}\";" : "");
366 if ($this->encoding) {
367 $headers['Content-Transfer-Encoding'] = $this->encoding;
368 }
369 if ($this->disposition) {
370 $headers['Content-Disposition'] = $this->disposition
371 . ($this->filename ? "; filename=\"{$this->filename}\"" : '');
372 }
373 return array_map(array($this, 'encodeHeader'), $headers);
374 }
375
376 public function hasBody()
377 {
378 if (is_null($this->content) && !$this->isType('multipart')) {
379 return false;
380 }
381 return true;
382 }
383
384 public function get($with_headers = false)
385 {
386 $content = "";
387 if ($with_headers) {
388 foreach ($this->getHeaders() as $key => $value) {
e1debf92 389 $line = "$key: $value";
390 $line = explode("\n", wordwrap($line, Banana::$msgshow_wrap));
391 for ($i = 1 ; $i < count($line) ; $i++) {
392 $line[$i] = "\t" . $line[$i];
393 }
394 $content .= implode("\n", $line) . "\n";
395 }
7027794f 396 $content .= "\n";
397 }
398 if ($this->isType('multipart')) {
399 $this->setBoundary();
400 foreach ($this->multipart as &$part) {
401 $content .= "\n--{$this->boundary}\n" . $part->get(true);
402 }
403 $content .= "\n--{$this->boundary}--";
404 } else {
e1debf92 405 $content .= banana_wordwrap($this->body);
7027794f 406 }
407 return $content;
408 }
409
410 public function getText()
411 {
7027794f 412 $this->decodeContent();
413 return $this->body;
414 }
415
3c3a3ce3 416 public function toHtml()
417 {
e02edbfe 418 @list($type, $subtype) = $this->getType();
3c3a3ce3 419 if ($type == 'image') {
420 $part = $this->id ? $this->id : $this->filename;
632f2956 421 return '<img class="multipart" src="'
3c3a3ce3 422 . banana_htmlentities(Banana::$page->makeUrl(array('group' => Banana::$group,
423 'artid' => Banana::$artid,
424 'part' => $part)))
425 . '" alt="' . banana_htmlentities($this->filename) . '" />';
426 } elseif (!in_array($type, Banana::$msgshow_mimeparts)
427 && !in_array($this->content_type, Banana::$msgshow_mimeparts)) {
428 $part = $this->id ? $this->id : $this->filename;
429 if (!$part) {
430 $part = $this->content_type;
431 }
432 return '[' . Banana::$page->makeImgLink(array('group' => Banana::$group,
433 'artid' => Banana::$artid,
434 'part' => $part,
435 'text' => $this->filename ? $this->filename : $this->content_type,
436 'img' => 'save')) . ']';
579ce5c9 437 } elseif ($type == 'multipart' && ($subtype == 'mixed' || $subtype == 'report')) {
438 $text = '';
439 foreach ($this->multipart as &$part) {
440 $text .= $part->toHtml();
3c3a3ce3 441 }
579ce5c9 442 return $text;
443 } else {
3c3a3ce3 444 switch ($subtype) {
445 case 'html': return banana_formatHtml($this);
446 case 'enriched': case 'richtext': return banana_formatRichText($this);
447 default:
19fc7e1d 448 if ($type == 'message') { // we have a raw source of data (no specific pre-formatting)
449 return '<hr />' . utf8_encode(banana_formatPlainText($this));
3c3a3ce3 450 }
451 return banana_formatPlainText($this);
452 }
453 }
454 return null;
455 }
456
457 public function quote()
458 {
459 list($type, $subtype) = $this->getType();
460 if (in_array($type, Banana::$msgedit_mimeparts) || in_array($this->content_type, Banana::$msgedit_mimeparts)) {
461 if ($type == 'multipart' && ($subtype == 'mixed' || $subtype == 'report')) {
462 $text = '';
463 foreach ($this->multipart as &$part) {
464 $qt = $part->quote();
465 $qt = rtrim($qt);
466 if (!empty($text)) {
467 $text .= "\n" . banana_quote("", 1) . "\n";
468 }
469 $text .= $qt;
470 }
471 return $text;
472 }
473 switch ($subtype) {
474 case 'html': return banana_quoteHtml($this);
475 case 'enriched': case 'richtext': return banana_quoteRichText($this);
476 default: return banana_quotePlainText($this);
477 }
478 }
479 }
480
7027794f 481 protected function getType()
482 {
483 return explode('/', $this->content_type);
484 }
485
486 protected function isType($type, $subtype = null)
487 {
488 list($mytype, $mysub) = $this->getType();
489 return ($mytype == $type) && (is_null($subtype) || $mysub == $subtype);
490 }
491
492 public function isFlowed()
493 {
494 return $this->format == 'flowed';
495 }
496
497 public function getFilename()
498 {
499 return $this->filename;
500 }
501
502 protected function getParts($type, $subtype = null)
503 {
504 $parts = array();
505 if ($this->isType($type, $subtype)) {
506 return array($this);
507 } elseif ($this->isType('multipart')) {
508 foreach ($this->multipart as &$part) {
509 $parts = array_merge($parts, $part->getParts($type, $subtype));
510 }
511 }
512 return $parts;
513 }
514
7027794f 515 public function getFile($filename)
516 {
517 if ($this->filename == $filename) {
518 return $this;
519 } elseif ($this->isType('multipart')) {
520 foreach ($this->multipart as &$part) {
521 $file = $part->getFile($filename);
522 if (!is_null($file)) {
523 return $file;
524 }
525 }
526 }
527 return null;
528 }
529
530 public function getAttachments()
531 {
532 if (!is_null($this->filename)) {
533 return array($this);
534 } elseif ($this->isType('multipart')) {
535 $parts = array();
536 foreach ($this->multipart as &$part) {
537 $parts = array_merge($parts, $part->getAttachments());
538 }
539 return $parts;
540 }
541 return array();
542 }
543
a9defc17 544 public function getAlternatives()
545 {
546 $types =& Banana::$msgshow_mimeparts;
547 $names =& Banana::$mimeparts;
548 $source = null;
549 if (in_array('source', $types)) {
550 $source = @$names['source'] ? $names['source'] : 'source';
551 }
552 if (!$this->isType('multipart', 'alternative') && !$this->isType('multipart', 'related')) {
553 if ($source) {
554 $parts = array($this);
555 } else {
556 return array();
557 }
558 } else {
559 $parts =& $this->multipart;
560 }
561 $alt = array();
562 foreach ($parts as &$part) {
563 list($type, $subtype) = $part->getType();
564 $ct = $type . '/' . $subtype;
565 if (in_array($ct, $types) || in_array($type, $types)) {
566 if (isset($names[$ct])) {
567 $alt[$ct] = $names[$ct];
568 } elseif (isset($names[$type])) {
569 $alt[$ct] = $names[$type];
570 } else {
571 $alt[$ct] = $ct;
572 }
573 }
574 }
575 if ($source) {
576 $alt['source'] = $source;
577 }
578 return $alt;
579 }
580
7027794f 581 public function getPartById($id)
582 {
583 if ($this->id == $id) {
584 return $this;
585 } elseif ($this->isType('multipart')) {
586 foreach ($this->multipart as &$part) {
587 $res = $part->getPartById($id);
588 if (!is_null($res)) {
589 return $res;
590 }
591 }
592 }
593 return null;
594 }
595}
596
598a1c53 597// vim:set et sw=4 sts=4 ts=4 enc=utf-8:
7027794f 598?>