2 /********************************************************************************
3 * banana/mimepart.inc.php : class for MIME parts
4 * ------------------------
6 * This file is part of the banana distribution
7 * Copyright: See COPYING files that comes with this distribution
8 ********************************************************************************/
12 public $headers = null
; /* Should be protected */
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 private $signature = array();
25 private $multipart = null
;
27 protected function __construct($data = null
)
29 if ($data instanceof BananaMimePart
) {
30 foreach ($this as $key=>$value) {
31 $this->$key = $data->$key;
33 } elseif (!is_null($data)) {
34 $this->fromRaw($data);
38 protected function makeTextPart($body, $content_type, $encoding, $charset = null
, $format = 'fixed')
41 $this->charset
= $charset;
42 $this->encoding
= $encoding;
43 $this->content_type
= $content_type;
44 $this->format
= strtolower($format);
48 protected function makeDataPart($body, $content_type, $encoding, $filename, $disposition, $id = null
)
51 $this->content_type
= $content_type;
52 $this->encoding
= $encoding;
53 $this->filename
= $filename;
54 $this->disposition
= $disposition;
56 if (is_null($content_type) ||
$content_type == 'application/octet-stream') {
57 $this->decodeContent();
58 $this->content_type
= BananaMimePart
::getMimeType($this->body
, false
);
62 protected function makeFilePart(array $file, $content_type = null
, $disposition = 'attachment')
64 $body = file_get_contents($file['tmp_name']);
65 if ($body === false ||
strlen($body) != $file['size']) {
68 if (is_null($content_type) ||
$content_type == 'application/octet-stream') {
69 $content_type = BananaMimePart
::getMimeType($file['tmp_name']);
71 if (substr($content_type, 0, 5) == 'text/') {
75 $body = chunk_split(base64_encode($body));
77 $this->filename
= $file['name'];
78 $this->content_type
= $content_type;
79 $this->disposition
= $disposition;
81 $this->encoding
= $encoding;
85 protected function makeMultiPart($body, $content_type, $encoding, $boundary, $sign_protocole)
88 $this->content_type
= $content_type;
89 $this->encoding
= $encoding;
90 $this->boundary
= $boundary;
91 $this->signature
['protocole'] = $sign_protocole;
95 protected function convertToMultiPart()
97 if (!$this->isType('multipart', 'mixed')) {
98 $newpart = new BananaMimePart($this);
99 $this->content_type
= 'multipart/mixed';
100 $this->encoding
= '8bit';
101 $this->multipart
= array($newpart);
102 $this->headers
= null
;
103 $this->charset
= null
;
104 $this->disposition
= null
;
105 $this->filename
= null
;
106 $this->boundary
= null
;
108 $this->format
= null
;
113 public function addAttachment(array $file, $content_type = null
, $disposition = 'attachment')
115 if (!is_uploaded_file($file['tmp_name'])) {
118 $newpart = new BananaMimePart
;
119 if ($newpart->makeFilePart($file, $content_type, $disposition)) {
120 $this->convertToMultiPart();
121 $this->multipart
[] = $newpart;
127 public function getHeader($title, $filter = null
)
129 if (!isset($this->headers
[$title])) {
132 $header =& $this->headers
[$title];
133 if (is_null($filter)) {
134 return trim($header);
135 } elseif (preg_match($filter, $header, $matches)) {
136 return trim($matches[1]);
141 protected function fromRaw($data)
143 if (is_array($data)) {
144 if (array_key_exists('From', $data)) {
145 $this->headers
= array_map(array($this, 'decodeHeader'), array_change_key_case($data));
151 $lines = explode("\n", $data);
153 $headers = BananaMimePart
::parseHeaders($lines);
154 $this->headers
=& $headers;
155 if (empty($headers) ||
empty($lines)) {
158 $content = join("\n", $lines);
160 $test = trim($content);
166 $content_type = strtolower($this->getHeader('content-type', '/^\s*([^ ;]+?)(;|$)/'));
167 if (empty($content_type)) {
170 $content_type = 'text/plain';
171 $format = strtolower($this->getHeader('x-rfc2646', '/format="?([^ w@"]+?)"?\s*(;|$)/i'));
173 $encoding = strtolower($this->getHeader('content-transfer-encoding'));
174 $disposition = $this->getHeader('content-disposition', '/(inline|attachment)/i');
175 $boundary = $this->getHeader('content-type', '/boundary="?([^ "]+?)"?\s*(;|$)/i');
176 $charset = strtolower($this->getHeader('content-type', '/charset="?([^ "]+?)"?\s*(;|$)/i'));
177 $filename = $this->getHeader('content-disposition', '/filename="?([^ "]+?)"?\s*(;|$)/i');
178 $format = strtolower($this->getHeader('content-type', '/format="?([^ "]+?)"?\s*(;|$)/i'));
179 $id = $this->getHeader('content-id', '/<(.*?)>/');
180 $sign_protocole = strtolower($this->getHeader('content-type', '/protocol="?([^ "]+?)"?\s*(;|$)/i'));
181 if (empty($filename)) {
182 $filename = $this->getHeader('content-type', '/name="?([^"]+)"?/');
185 list($type, $subtype) = explode('/', $content_type);
187 case 'text': case 'message':
188 $this->makeTextPart($content, $content_type, $encoding, $charset, $format);
191 $this->makeMultiPart($content, $content_type, $encoding, $boundary, $sign_protocole);
194 $this->makeDataPart($content, $content_type, $encoding, $filename, $disposition, $id);
198 private function parse()
200 if ($this->isType('multipart')) {
201 $this->splitMultipart();
203 $parts = $this->findUUEncoded();
205 $this->convertToMultiPart();
206 $this->multipart
= array_merge($this->multipart
, $parts);
207 // Restore "message" headers to the previous level"
208 $this->headers
= array();
209 foreach (Banana
::$msgshow_headers as $hdr) {
210 if (isset($this->multipart
[0]->headers
[$hdr])) {
211 $this->headers
[$hdr] = $this->multipart
[0]->headers
[$hdr];
218 private function splitMultipart()
220 $this->decodeContent();
221 if (is_null($this->multipart
)) {
222 $this->multipart
= array();
224 $boundary =& $this->boundary
;
225 $parts = preg_split("/(^|\n)--" . preg_quote($boundary, '/') . "(--|\n)/", $this->body
, -1, PREG_SPLIT_NO_EMPTY
);
226 $signed = $this->isType('multipart', 'signed');
228 $signed_message = null
;
229 foreach ($parts as &$part) {
230 $newpart = new BananaMimePart($part);
231 if (!is_null($newpart->content_type
)) {
232 if ($signed && $newpart->content_type
== $this->signature
['protocole']) {
233 $signature = $newpart->body
;
235 $signed_message = $part;
237 $this->multipart
[] = $newpart;
241 $this->checkPGPSignature($signature, $signed_message);
246 public static function getMimeType($data, $is_filename = true
)
249 $type = mime_content_type($data);
250 if ($type == 'text/plain') { // XXX Workaround a bug of php 5.2.0+etch10 (fallback for mime_content_type is text/plain)
251 $type = preg_replace('/;.*/', '', trim(shell_exec('file -bi ' . escapeshellarg($data))));
254 $arg = escapeshellarg($data);
255 $type = preg_replace('/;.*/', '', trim(shell_exec("echo $arg | file -bi -")));
257 return empty($type) ?
'application/octet-stream' : $type;
260 private function findUUEncoded()
262 $this->decodeContent();
264 if (preg_match_all("/\n(begin \d+ ([^\r\n]+)\r?(?:\n(?!end)[^\n]*)*\nend)/",
265 $this->body
, $matches, PREG_SET_ORDER
)) {
266 foreach ($matches as &$match) {
267 $data = convert_uudecode($match[1]);
268 $mime = BananaMimePart
::getMimeType($data, false
);
269 if ($mime != 'application/x-empty') {
270 $this->body
= trim(str_replace($match[0], '', $this->body
));
271 $newpart = new BananaMimePart
;
272 self
::decodeHeader($match[2]);
273 $newpart->makeDataPart($data, $mime, '8bit', $match[2], 'attachment');
281 static private function _decodeHeader($charset, $c, $str)
283 $s = ($c == 'Q' ||
$c == 'q') ?
quoted_printable_decode($str) : base64_decode($str);
284 $s = @iconv
($charset, 'UTF-8', $s);
285 return str_replace('_', ' ', $s);
288 static public function decodeHeader(&$val, $key = null
)
290 if (preg_match('/[\x80-\xff]/', $val)) {
291 if (!is_utf8($val)) {
292 $val = utf8_encode($val);
294 } elseif (strpos($val, '=') !== false
) {
295 $val = preg_replace('/(=\?.*?\?[bq]\?.*?\?=) (=\?.*?\?[bq]\?.*?\?=)/i', '\1\2', $val);
296 $val = preg_replace('/=\?(.*?)\?([bq])\?(.*?)\?=/ie', 'BananaMimePart::_decodeHeader("\1", "\2", "\3")', $val);
300 static public function &parseHeaders(array &$lines)
304 $line = array_shift($lines);
305 if (isset($hdr) && $line && ctype_space($line{0})) {
306 $headers[$hdr] .= ' ' . trim($line);
307 } elseif (!empty($line)) {
308 if (strpos($line, ':') !== false
) {
309 list($hdr, $val) = explode(":", $line, 2);
310 $hdr = strtolower($hdr);
311 if (in_array($hdr, Banana
::$msgparse_headers)) {
312 $headers[$hdr] = ltrim($val);
321 array_walk($headers, array('BananaMimePart', 'decodeHeader'));
325 static public function encodeHeader($value, $trim = 0)
328 if (strlen($value) > $trim) {
329 $value = substr($value, 0, $trim);
332 $value = preg_replace('/([\x80-\xff]+)/e', '"=?UTF-8?B?" . base64_encode("\1") . "?="', $value);
336 private function decodeContent()
338 $encodings = Array('quoted-printable' => 'quoted_printable_decode',
339 'base64' => 'base64_decode',
340 'x-uuencode' => 'convert_uudecode');
341 foreach ($encodings as $encoding => $callback) {
342 if ($this->encoding
== $encoding) {
343 $this->body
= $callback($this->body
);
344 $this->encoding
= '8bit';
348 if (!$this->isType('text')) {
352 if (!is_null($this->charset
)) {
353 $body = @iconv
($this->charset
, 'UTF-8//IGNORE', $this->body
);
359 $this->body
= utf8_encode($this->body
);
361 $this->charset
= 'utf-8';
364 public function send($force_inline = false
)
366 $this->decodeContent();
368 $dispostion = $this->disposition
;
369 $this->disposition
= 'inline';
371 $headers = $this->getHeaders();
372 foreach ($headers as $key => $value) {
373 header("$key: $value");
376 $this->disposition
= $disposition;
382 private function setBoundary()
384 if ($this->isType('multipart') && is_null($this->boundary
)) {
385 $this->boundary
= '--banana-bound-' . time() . rand(0, 255) . '-';
389 public function getHeaders()
392 $this->setBoundary();
393 $headers['Content-Type'] = $this->content_type
. ";"
394 . ($this->filename ?
" name=\"{$this->filename}\";" : '')
395 . ($this->charset ?
" charset=\"{$this->charset}\";" : '')
396 . ($this->boundary ?
" boundary=\"{$this->boundary}\";" : "")
397 . ($this->format ?
" format={$this->format}" : "");
398 if ($this->encoding
) {
399 $headers['Content-Transfer-Encoding'] = $this->encoding
;
401 if ($this->disposition
) {
402 $headers['Content-Disposition'] = $this->disposition
403 . ($this->filename ?
"; filename=\"{$this->filename}\"" : '');
405 return array_map(array($this, 'encodeHeader'), $headers);
408 public function hasBody()
410 if (is_null($this->content
) && !$this->isType('multipart')) {
416 public function get($with_headers = false
)
420 foreach ($this->getHeaders() as $key => $value) {
421 $line = "$key: $value";
422 $line = explode("\n", wordwrap($line, Banana
::$msgshow_wrap));
423 for ($i = 1 ; $i < count($line) ; $i++
) {
424 $line[$i] = "\t" . $line[$i];
426 $content .= implode("\n", $line) . "\n";
430 if ($this->isType('multipart')) {
431 $this->setBoundary();
432 foreach ($this->multipart
as &$part) {
433 $content .= "\n--{$this->boundary}\n" . $part->get(true
);
435 $content .= "\n--{$this->boundary}--";
436 } elseif ($this->isType('text', 'plain')) {
437 $content .= banana_flow($this->body
);
439 $content .= banana_wordwrap($this->body
);
444 public function getText()
446 $signed =& $this->getSignedPart();
447 if ($signed !== $this) {
448 return $signed->getText();
450 $this->decodeContent();
454 public function toHtml()
456 $signed =& $this->getSignedPart();
457 if ($signed !== $this) {
458 return $signed->toHtml();
460 @list
($type, $subtype) = $this->getType();
461 if ($type == 'image') {
462 $part = $this->id ?
$this->id
: $this->filename
;
463 return '<img class="multipart" src="'
464 . banana_htmlentities(Banana
::$page->makeUrl(array('group' => Banana
::$group,
465 'artid' => Banana
::$artid,
467 . '" alt="' . banana_htmlentities($this->filename
) . '" />';
468 } else if ($type == 'multipart' && $subtype == 'alternative') {
469 $types =& Banana
::$msgshow_mimeparts;
470 foreach ($types as $type) {
471 @list
($type, $subtype) = explode('/', $type);
472 $part = $this->getParts($type, $subtype);
473 if (count($part) > 0) {
474 return $part[0]->toHtml();
477 } elseif ((!in_array($type, Banana
::$msgshow_mimeparts)
478 && !in_array($this->content_type
, Banana
::$msgshow_mimeparts))
479 ||
$this->disposition
== 'attachment') {
480 $part = $this->id ?
$this->id
: $this->filename
;
482 $part = $this->content_type
;
484 return '[' . Banana
::$page->makeImgLink(array('group' => Banana
::$group,
485 'artid' => Banana
::$artid,
487 'text' => $this->filename ?
$this->filename
: $this->content_type
,
488 'img' => 'save')) . ']';
489 } elseif ($type == 'multipart' && ($subtype == 'mixed' ||
$subtype == 'report')) {
491 foreach ($this->multipart
as &$part) {
492 $text .= $part->toHtml();
497 case 'html': return banana_formatHtml($this);
498 case 'enriched': case 'richtext': return banana_formatRichText($this);
500 if ($type == 'message') { // we have a raw source of data (no specific pre-formatting)
501 return '<hr />' . utf8_encode(banana_formatPlainText($this));
503 return banana_formatPlainText($this);
509 public function quote()
511 $signed =& $this->getSignedPart();
512 if ($signed !== $this) {
513 return $signed->quote();
515 list($type, $subtype) = $this->getType();
516 if (in_array($type, Banana
::$msgedit_mimeparts) ||
in_array($this->content_type
, Banana
::$msgedit_mimeparts)) {
517 if ($type == 'multipart' && ($subtype == 'mixed' ||
$subtype == 'report')) {
519 foreach ($this->multipart
as &$part) {
520 $qt = $part->quote();
523 $text .= "\n" . banana_quote("", 1) . "\n";
530 case 'html': return banana_quoteHtml($this);
531 case 'enriched': case 'richtext': return banana_quoteRichText($this);
532 default: return banana_quotePlainText($this);
537 protected function getType()
539 return explode('/', $this->content_type
);
542 protected function isType($type, $subtype = null
)
544 list($mytype, $mysub) = $this->getType();
545 return ($mytype == $type) && (is_null($subtype) ||
$mysub == $subtype);
548 public function isFlowed()
550 return $this->format
== 'flowed';
553 public function getFilename()
555 return $this->filename
;
558 protected function getParts($type, $subtype = null
)
561 if ($this->isType($type, $subtype)) {
563 } elseif ($this->isType('multipart')) {
564 foreach ($this->multipart
as &$part) {
565 $parts = array_merge($parts, $part->getParts($type, $subtype));
571 public function getFile($filename)
573 if ($this->filename
== $filename) {
575 } elseif ($this->isType('multipart')) {
576 foreach ($this->multipart
as &$part) {
577 $file = $part->getFile($filename);
578 if (!is_null($file)) {
586 public function getAttachments()
588 if (!is_null($this->filename
)) {
590 } elseif ($this->isType('multipart')) {
592 foreach ($this->multipart
as &$part) {
593 $parts = array_merge($parts, $part->getAttachments());
600 public function getAlternatives()
602 $types =& Banana
::$msgshow_mimeparts;
603 $names =& Banana
::$mimeparts;
605 if (in_array('source', $types)) {
606 $source = @$names['source'] ?
$names['source'] : 'source';
608 if ($this->isType('multipart', 'signed')) {
609 $parts = array($this->getSignedPart());
610 } else if (!$this->isType('multipart', 'alternative') && !$this->isType('multipart', 'related')) {
612 $parts = array($this);
617 $parts =& $this->multipart
;
620 foreach ($parts as &$part) {
621 list($type, $subtype) = $part->getType();
622 $ct = $type . '/' . $subtype;
623 if (in_array($ct, $types) ||
in_array($type, $types)) {
624 if (isset($names[$ct])) {
625 $alt[$ct] = $names[$ct];
626 } elseif (isset($names[$type])) {
627 $alt[$ct] = $names[$type];
634 $alt['source'] = $source;
639 public function getPartById($id)
641 if ($this->id
== $id) {
643 } elseif ($this->isType('multipart')) {
644 foreach ($this->multipart
as &$part) {
645 $res = $part->getPartById($id);
646 if (!is_null($res)) {
654 protected function &getSignedPart()
656 if ($this->isType('multipart', 'signed')) {
657 foreach ($this->multipart
as &$part) {
658 if ($part->content_type
!= $this->signature
['protocole']) {
666 private function checkPGPSignature($signature, $message = null
)
668 if (!Banana
::$msgshow_pgpcheck) {
671 $signname = tempnam(Banana
::$spool_root, 'banana_pgp_');
672 $gpg = 'LC_ALL="en_US" ' . Banana
::$msgshow_pgppath . ' ' . Banana
::$msgshow_pgpoptions . ' --verify '
673 . $signname . '.asc ';
674 file_put_contents($signname. '.asc', $signature);
675 $gpg_check = array();
676 if (!is_null($message)) {
677 file_put_contents($signname, str_replace(array("\r\n", "\n"), array("\n", "\r\n"), $message));
678 exec($gpg . $signname . ' 2>&1', $gpg_check, $result);
681 exec($gpg . '2&>1', $gpg_check, $result);
683 unlink("$signname.asc");
684 if (preg_match('/Signature made (.+) using (.+) key ID (.+)/', array_shift($gpg_check), $matches)) {
685 $this->signature
['date'] = strtotime($matches[1]);
686 $this->signature
['key'] = array('format' => $matches[2],
687 'id' => $matches[3]);
691 $signature = array_shift($gpg_check);
692 if (preg_match('/Good signature from "(.+)"/', $signature, $matches)) {
693 $this->signature
['verify'] = true
;
694 $this->signature
['identity'] = array($matches[1]);
695 $this->signature
['certified'] = true
;
696 } elseif (preg_match('/BAD signature from "(.+)"/', $signature, $matches)) {
697 $this->signature
['verify'] = false
;
698 $this->signature
['identity'] = array($matches[1]);
699 $this->signature
['certified'] = false
;
703 foreach ($gpg_check as $aka) {
704 if (preg_match('/aka "(.+)"/', $aka, $matches)) {
705 $this->signature
['identity'][] = $matches[1];
707 if (preg_match('/This key is not certified with a trusted signature!/', $aka)) {
708 $this->signature
['certified'] = false
;
709 $this->signature
['certification_error'] = _b_("identité non confirmée");
715 public function getSignature()
717 return $this->signature
;
721 // vim:set et sw=4 sts=4 ts=4 enc=utf-8: