root/trunk/mime.class.php

Revision 25, 20.6 kB (checked in by bobe, 2 years ago)

Pliage des entêtes rendus optionnel (activé par défaut)

  • Property svn:keywords set to Author Date Id Revision
Line 
1 <?php
2 /**
3  * Copyright (c) 2002-2006 Aurélien Maille
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  *
19  * @package Wamailer
20  * @author  Bobe <wascripts@phpcodeur.net>
21  * @link    http://phpcodeur.net/wascripts/wamailer/
22  * @license http://www.gnu.org/copyleft/lesser.html
23  * @version $Id$
24  *
25  * @see RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies
26  * @see RFC 2046 - Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types
27  * @see RFC 2047 - Multipurpose Internet Mail Extensions (MIME) Part Three: Message Header Extensions for Non-ASCII Text
28  * @see RFC 2048 - Multipurpose Internet Mail Extensions (MIME) Part Four: Registration Procedures
29  * @see RFC 2049 - Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples
30  * @see RFC 2076 - Common Internet Message Headers
31  * @see RFC 2111 - Content-ID and Message-ID Uniform Resource Locators
32  * @see RFC 2183 - Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
33  * @see RFC 2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations
34  * @see RFC 2822 - Internet Message Format
35  *
36  * Les sources qui m’ont bien aidées :
37  *
38  * @link http://abcdrfc.free.fr/ (français)
39  * @link http://www.faqs.org/rfcs/ (anglais)
40  */
41
42 class Mime {
43     
44     /**
45      * Utilisé dans la méthode Mime::encodeHeader() pour détecter
46      * si un octet donné est le début d’une séquence d’octets utf-8
47      *
48      * @static
49      * @var array
50      * @access private
51      */
52     private static $_utf8test = array(
53         0x80 => 0, 0xE0 => 0xC0, 0xF0 => 0xE0, 0xF8 => 0xF0, 0xFC => 0xF8, 0xFE => 0xFC
54     );
55     
56     /**
57      * Encode le texte au format quoted-printable tel que défini dans la RFC 2045
58      *
59      * @see RFC 2045 (sec. 6.7)
60      *
61      * @param string $str
62      *
63      * @static
64      * @access public
65      * @return string
66      */
67     public static function quotedPrintableEncode($str)
68     {
69         $str = preg_replace('/\r\n?|\n/', "\r\n", $str);
70         $str = preg_replace('/([^\x09\x0A\x0D\x20-\x3C\x3E-\x7E]|[\x09\x20](?=\x0D\x0A|$))/e',
71             'sprintf(\'=%02X\', ord("\\1"));', $str);
72         
73         $maxlen = 76;
74         $lines  = explode("\r\n", $str);
75         
76         foreach( $lines as &$line ) {
77             if( ($strlen = strlen($line)) == 0 ) {
78                 continue;
79             }
80             
81             $newline = '';
82             $pos = 0;
83             
84             while( $pos < $strlen ) {
85                 $tmplen = $maxlen;
86                 $i = min(($pos + $tmplen), $strlen);
87                 
88                 // Si une coupure est nécessaire, on fait de la place pour
89                 // le signe égal, "soft break"
90                 if( $i < $strlen ) {
91                     $tmplen--;
92                     $i--;
93                 }
94                 
95                 while( $line{$i-1} == '=' || $line{$i-2} == '=' ) {
96                     $tmplen--;
97                     $i--;
98                 }
99                 
100                 $newline .= substr($line, $pos, $tmplen) . "=\r\n";
101                 $pos += $tmplen;
102             }
103             
104             $line = rtrim($newline, "=\r\n");
105         }
106         
107         return implode("\r\n", $lines);
108     }
109     
110     /**
111      * Encode la valeur d’un en-tête si elle contient des caractères non-ascii.
112      * Autrement, des guillemets peuvent néanmoins être ajoutés aux extrémités
113      * si des caractères interdits pour le token considéré sont présents.
114      *
115      * @param string $header   Nom de l’en-tête concerné
116      * @param string $header   Valeur d’en-tête à encoder
117      * @param string $charset  Jeu de caractères utilisé
118      * @param string $token
119      *
120      * @static
121      * @access public
122      * @return string
123      */
124     public static function encodeHeader($name, $value, $charset, $token = 'text')
125     {
126         if( preg_match('/[\x00-\x1F\x7F-\xFF]/', $value) ) {
127             
128             $maxlen = 76;
129             $sep = "\r\n\t";
130             
131             switch( $token ) {
132                 case 'comment':
133                     $charlist = '\x00-\x1F\x22\x28\x29\x3A\x3D\x3F\x5F\x7F-\xFF';
134                     break;
135                 case 'phrase':
136                     $charlist = '\x00-\x1F\x22-\x29\x2C\x2E\x3A\x40\x5B-\x60\x7B-\xFF';
137                     break;
138                 case 'text':
139                 default:
140                     $charlist = '\x00-\x1F\x3A\x3D\x3F\x5F\x7F-\xFF';
141                     break;
142             }
143             
144             /**
145              * Si le nombre d’octets à encoder représente plus de 33% de la chaîne,
146              * nous utiliserons l’encodage base64 qui garantit une chaîne encodée 33%
147              * plus longue que l’originale, sinon, on utilise l’encodage "Q".
148              * La RFC 2047 recommande d’utiliser pour chaque cas l’encodage produisant
149              * le résultat le plus court.
150              *
151              * @see RFC 2045#6.8
152              * @see RFC 2047#4
153              */
154             $q = preg_match_all("/[$charlist]/", $value, $matches);
155             $strlen   = strlen($value);
156             $encoding = (($q / $strlen) < 0.33) ? 'Q' : 'B';
157             $template = sprintf('=?%s?%s?%%s?=%s', $charset, $encoding, $sep);
158             $maxlen   = ($maxlen - strlen($template) + strlen($sep) + 2);// + 2 pour le %s dans le modèle
159             $is_utf8  = (strcasecmp($charset, 'UTF-8') == 0);
160             $newbody  = '';
161             $pos = 0;
162             
163             while( $pos < $strlen ) {
164                 $tmplen = $maxlen;
165                 if( $newbody == '' ) {
166                     $tmplen -= strlen($name . ': ');
167                     if( $encoding == 'Q' ) $tmplen++;// TODO : à comprendre
168                 }
169                 
170                 if( $encoding == 'Q' ) {
171                     $q = preg_match_all("/[$charlist]/", substr($value, $pos, $tmplen), $matches);
172                     // chacun des octets trouvés prendra trois fois plus de place dans
173                     // la chaîne encodée. On retranche cette valeur de la longueur du tronçon
174                     $tmplen -= ($q * 2);
175                 }
176                 else {
177                     /**
178                      * La longueur de l'encoded-text' doit être un multiple de 4
179                      * pour ne pas casser l’encodage base64
180                      *
181                      * @see RFC 2047#5
182                      */
183                     $tmplen -= ($tmplen % 4);
184                     $tmplen = floor(($tmplen/4)*3);
185                 }
186                 
187                 if( $is_utf8 ) {
188                     /**
189                      * Il est interdit de sectionner un caractère multi-octet.
190                      * On teste chaque octet en partant de la fin du tronçon en cours
191                      * jusqu’à tomber sur un caractère ascii ou l’octet de début de
192                      * séquence d’un caractère multi-octets.
193                      * On vérifie alors qu’il y bien $m octets qui suivent (le cas échéant).
194                      * Si ce n’est pas le cas, on réduit la longueur du tronçon.
195                      *
196                      * @see RFC 2047#5
197                      */
198                     for( $i = min(($pos + $tmplen), $strlen), $c = 1; $i > $pos; $i--, $c++ ) {
199                         $d = ord($value{$i-1});
200                         
201                         reset(self::$_utf8test);
202                         for( $m = 1; $m <= 6; $m++ ) {
203                             $test = each(self::$_utf8test);
204                             if( ($d & $test[0]) == $test[1] ) {
205                                 if( $c < $m ) {
206                                     $tmplen -= $c;
207                                 }
208                                 break 2;
209                             }
210                         }
211                     }
212                 }
213                 
214                 $tmp = substr($value, $pos, $tmplen);
215                 if( $encoding == 'Q' ) {
216                     $tmp = preg_replace("/([$charlist])/e", 'sprintf(\'=%02X\', ord("\\1"));', $tmp);
217                     $tmp = str_replace(' ', '_', $tmp);
218                 }
219                 else {
220                     $tmp = base64_encode($tmp);
221                 }
222                 
223                 $newbody .= sprintf($template, $tmp);
224                 $pos += $tmplen;
225             }
226             
227             $value = rtrim($newbody);
228         }
229         else if( $token != 'text' ) {
230             if( preg_match('/[^!#$%&\'*+\/0-9=?a-z^_`{|}~-]/', $value) ) {
231                 $value = '"'.$value.'"';
232             }
233         }
234         
235         return $value;
236     }
237     
238     /**
239      * @param string  $str
240      * @param integer $maxlen
241      *
242      * @static
243      * @access public
244      * @return string
245      */
246     public static function wordwrap($str, $maxlen = 78)
247     {
248         if( strlen($str) > $maxlen ) {
249             $lines = explode("\r\n", $str);
250             foreach( $lines as &$line ) {
251                 $line = wordwrap($line, $maxlen, "\r\n");
252             }
253             $str = implode("\r\n", $lines);
254         }
255         
256         return $str;
257     }
258     
259     /**
260      * @param string $filename
261      *
262      * @static
263      * @access public
264      * @return string
265      */
266     public static function getType($filename)
267     {
268         if( !is_readable($filename) ) {
269             throw new Exception("Cannot read file '$filename'");
270         }
271         
272         if( extension_loaded('fileinfo') ) {
273             $info = new finfo(FILEINFO_MIME);
274             $type = $info->file($filename);
275         }
276         else if( function_exists('exec') ) {
277             $type = exec(sprintf('file -biL %s 2>/dev/null',
278                 escapeshellarg($filename)), $null, $result);
279             
280             if( $result !== 0 || !strpos($type, '/') ) {
281                 $type = '';
282             }
283 /*            else {
284                 if( strpos($type, ';') ) {
285                     list($type) = explode(';', $type);
286                 }
287             }*/
288         }
289         else if( extension_loaded('mime_magic') ) {
290             $type = mime_content_type($filename);
291         }
292         
293         if( empty($type) ) {
294             $type = 'application/octet-stream';
295         }
296         
297         return trim($type);
298     }
299 }
300
301 class Mime_Part {
302     
303     /**
304      * Bloc d’en-têtes de cette partie
305      *
306      * @var object
307      * @see Mime_Headers class
308      * @access public
309      */
310     public $headers   = null;
311     
312     /**
313      * Contenu de cette partie
314      *
315      * @var mixed
316      * @access public
317      */
318     public $body      = null;
319     
320     /**
321      * tableau des éventuelles sous-parties
322      *
323      * @var mixed
324      * @access public
325      */
326     private $subparts = array();
327     
328     /**
329      * Frontière de séparation entre les différentes sous-parties
330      *
331      * @var string
332      * @access private
333      */
334     private $boundary  = null;
335     
336     /**
337      * Limitation de longueur des lignes de texte.
338      * Par défaut, la limitation est celle imposée par la RFC2822,
339      * à savoir 998 octets + CRLF
340      * Si cet attribut est placé à true, la limitation est de 78
341      * octets + CRLF
342      *
343      * @var boolean
344      * @access public
345      */
346     public $wraptext  = true;
347     
348     /**
349      * Constructeur de classe
350      *
351      * @param string $body
352      *
353      * @access public
354      * @return void
355      */
356     public function __construct($body = null, $headers = null)
357     {
358         $this->headers = new Mime_Headers($headers);
359         
360         if( !is_null($body) ) {
361             $this->body = $body;
362         }
363     }
364     
365     /**
366      * Ajout de sous-partie(s) à ce bloc MIME
367      *
368      * @param mixed $subpart  Peut être un objet Mime_Part, un tableau
369      *                        d’objets Mime_Part, ou simplement une chaîne
370      *
371      * @access public
372      * @return void
373      */
374     public function addSubPart($subpart)
375     {
376         if( is_array($subpart) ) {
377             $this->subparts = array_merge($this->subparts, $subpart);
378         }
379         else {
380             array_push($this->subparts, $subpart);
381         }
382     }
383     
384     /**
385      * Indique si ce bloc MIME contient des sous-parties
386      *
387      * @access public
388      * @return boolean
389      */
390     public function isMultiPart()
391     {
392         return count($this->subparts) > 0;
393     }
394     
395     /**
396      * @access public
397      * @return string
398      */
399     public function __toString()
400     {
401         if( $this->headers->get('Content-Type') == null ) {
402             $this->headers->set('Content-Type', 'application/octet-stream');
403         }
404         
405         $body = $this->body;
406         
407         if( $this->isMultiPart() ) {
408             $this->boundary = '--=_Part_' . md5(microtime());
409             $this->headers->get('Content-Type')->param('boundary', $this->boundary);
410             
411             if( $body != '' ) {
412                 $body .= "\r\n\r\n";
413             }
414             
415             foreach( $this->subparts as $subpart ) {
416                 $body .= '--' . $this->boundary . "\r\n";
417                 $body .= !is_string($subpart) ? $subpart->__toString() : $subpart;
418                 $body .= "\r\n";
419             }
420             
421             $body .= '--' . $this->boundary . "--\r\n";
422         }
423         else {
424             if( $encoding = $this->headers->get('Content-Transfer-Encoding') ) {
425                 $encoding = strtolower($encoding->value);
426             }
427             
428             if( !in_array($encoding, array('7bit', '8bit', 'quoted-printable', 'base64', 'binary')) ) {
429                 $this->headers->remove('Content-Transfer-Encoding');
430                 $encoding = '7bit';
431             }
432             
433             switch( $encoding ) {
434                 case 'quoted-printable':
435                     /**
436                      * Encodage en chaîne à guillemets
437                      *
438                      * @see RFC 2045#6.7
439                      */
440                     $body = Mime::quotedPrintableEncode($body);
441                     break;
442                 case 'base64':
443                     /**
444                      * Encodage en base64
445                      *
446                      * @see RFC 2045#6.8
447                      */
448                     $body = rtrim(chunk_split(base64_encode($body)));
449                     break;
450                 case '7bit':
451                 case '8bit':
452                     $body = preg_replace("/\r\n?|\n/", "\r\n", $body);
453                     
454                     /**
455                      * Limitation sur les longueurs des lignes de texte.
456                      * La limite basse est de 78 caractères par ligne.
457                      * En tout état de cause, chaque ligne ne DOIT PAS
458                      * faire plus de 998 caractères.
459                      *
460                      * @see RFC 2822#2.1.1
461                      */
462                     $body = Mime::wordwrap($body, $this->wraptext ? 78 : 998);
463                     break;
464             }
465         }
466         
467         return $this->headers->__toString() . "\r\n" . $body;
468     }
469     
470     private function __set($name, $value)
471     {
472         if( $name == 'encoding' ) {
473             $this->headers->set('Content-Transfer-Encoding', $value);
474         }
475     }
476     
477     private function __get($name)
478     {
479         $value = null;
480         
481         if( $name == 'encoding' ) {
482             if( $encoding = $this->headers->get('Content-Transfer-Encoding') ) {
483                 $value = $encoding->value;
484             }
485             else {
486                 $value = '7bit';
487             }
488         }
489         
490         return $value;
491     }
492     
493     public function __clone()
494     {
495         $this->headers = clone $this->headers;
496         
497         if( is_array($this->subparts) ) {
498             foreach( $this->subparts as &$subpart ) {
499                 $subpart = clone $subpart;
500             }
501         }
502     }
503 }
504
505 class Mime_Headers implements Iterator {
506     
507     /**
508      * Tableau d’en-têtes
509      *
510      * @var array
511      * @access private
512      */
513     private $headers = array();
514     
515     private $_it_tot = 0;
516     private $_it_ind = 0;
517     private $_it_obj = null;
518     
519     /**
520      * Constructeur de classe
521      *
522      * @param array $headers  Tableau d’en-têtes d’email à ajouter dans l’objet
523      *
524      * @access public
525      * @return void
526      */
527     public function __construct($headers = null)
528     {
529         if( is_array($headers) ) {
530             foreach( $headers as $name => $value ) {
531                 $this->add($name, $value);
532             }
533         }
534     }
535     
536     /**
537      * Ajout d’un en-tête
538      *
539      * @param string $name   Nom de l’en-tête
540      * @param string $value  Valeur de l’en-tête
541      *
542      * @access public
543      * @return Mime_Header
544      */
545     public function add($name, $value)
546     {
547         $header = new Mime_Header($name, $value);
548         $name   = strtolower($header->name);
549         
550         if( $this->get($name) != null ) {
551             if( !is_array($this->headers[$name]) ) {
552                 $this->headers[$name] = array($this->headers[$name]);
553             }
554             
555             array_push($this->headers[$name], $header);
556         }
557         else {
558             $this->headers[$name] = $header;
559         }
560         
561         return $header;
562     }
563     
564     /**
565      * Ajout d’un en-tête, en écrasant si besoin la valeur précédemment affectée
566      * à l’en-tête de même nom
567      *
568      * @p