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