fix http host set by caller
[Pman.Core] / Mailer.php
1 <?php
2
3 /**
4  *
5  *  code that used to be in Pman (sendTemplate / emailTemplate)
6  * 
7  *  template is in template directory subfolder 'mail'
8  *   
9  *  eg. use 'welcome' as template -> this will use templates/mail/welcome.txt
10  *  if you also have templates/mail/welcome.body.html - then that will be used as 
11  *     the html body
12  * 
13  *
14  *  usage:
15  *
16  * 
17  *  require_once 'Pman/Core/Mailer.php';
18  *  $x= new  Pman_Core_Mailer(array(
19        'page' => $this,
20                 // if bcc is property of this, then it will be used (BAD DESIGN)
21        'rcpts' => array(),    
22        'template' => 'your_template',
23                 // must be in templates/mail direcotry..
24                 // header and plaintext verison in mail/your_template.txt
25                 // if you want a html body - use  mail/your_template.body.html
26        
27         // 'bcc' => 'xyz@abc.com,abc@xyz.com',  // string...
28         // 'contents'  => array(),              //  << keys must be trusted
29                                             // if bcc is property of contents, then it will be used (BAD DESIGN)
30            
31         // 'html_locale => 'en',                // always use the 'english translated verison'
32         // 'cache_images => true,               // -- defaults to caching images - set to false to disable.
33         // 'replaceImages => false,             // should images be replaced.
34         // 'urlmap => array(                    // map urls from template to a different location.
35         //      'https://www.mysite.com/' => 'http://localhost/',
36         // ),
37         // 'locale' => 'en',                    // .... or zh_hk....
38            
39         // 'attachments' => array(
40         //       array(
41         //        'file' => '/path/to/file',    // file location
42         //        name => 'myfile.pdf',         // (optional) - uses basename of file
43         //        mimetype : 
44         //      ), 
45         //  
46         // 'mail_method' =>  'SMTP',            // or SMTPMX
47   
48     )
49  *
50  *  recipents is gathered from the resulting template
51  *   -- eg.
52  *    To: <a>,<b>,<c>
53  * 
54  * 
55  *  if the file     '
56  * 
57  * 
58  *  $x->toData(); // returns data needed for notify?? - notify should really
59  *                  // just use this to pass around later..
60  *
61  *  $x->send();
62  *
63  */
64
65 class Pman_Core_Mailer {
66     var $debug          = 0;
67     var $page           = false; /* usually a html_flexyframework_page */
68     var $contents       = array(); /* object or array */
69     var $template       = false; /* string */
70     var $replaceImages  = false; /* boolean */
71     var $rcpts   = false;
72     var $templateDir = false;
73     var $locale = false; // eg. 'en' or 'zh_HK'
74     var $urlmap = array();
75     
76     var $htmlbody;
77     var $textbody;
78     
79     var $html_locale = false; // eg. 'en' or 'zh_HK'
80     var $images         = array(); // generated list of cid images for sending
81     var $attachments = false;
82     var $css_inline = false; // put the css into the html
83     var $css_embed = false; // put the css tags into the body.
84     
85     var $mail_method = 'SMTP';
86     
87     var $cache_images = true;
88       
89     var $bcc = false;
90     
91     var $body_cls = false;
92     
93     function __construct($args) {
94         foreach($args as $k=>$v) {
95             // a bit trusting..
96             $this->$k =  $v;
97         }
98         // allow core mailer debug setting.
99         $ff = HTML_FlexyFramework::get();
100         
101         if (!empty($ff->Core_Mailer['debug'])) {
102             $this->debug = $ff->Core_Mailer['debug'];
103         }
104         //$this->log("URL MAP");
105         //$this->log($this->urlmap);
106         
107     }
108      
109     /**
110      * ---------------- Global Tools ---------------
111      *
112      * applies this variables to a object
113      * msgid
114      * HTTP_HOIST
115      * 
116      */
117     
118     function toData()
119     {
120         $templateFile = $this->template;
121         $args = (array)$this->contents;
122         $content  = clone($this->page);
123         
124         foreach($args as $k=>$v) {
125             $content->$k = $v;
126         }
127         
128         $content->msgid = empty($content->msgid ) ? md5(time() . rand()) : $content->msgid ;
129         
130         // content can override this now
131         $ff = HTML_FlexyFramework::get();
132         $http_host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : 'pman.HTTP_HOST.not.set';
133         if (isset($ff->Pman['HTTP_HOST']) && $http_host != 'localhost') {
134             $http_host  = $ff->Pman['HTTP_HOST'];
135         }
136         if (empty($content->HTTP_HOST )) {
137             $content->HTTP_HOST = $http_host;
138         }
139         
140         // this should be done by having multiple template sources...!!!
141         
142         require_once 'HTML/Template/Flexy.php';
143         
144         $tmp_opts = array(
145            // 'forceCompile' => true,
146             'site_prefix' => false,
147             'multiSource' => true,
148         );
149         
150         $fopts = HTML_FlexyFramework::get()->HTML_Template_Flexy;
151         
152         //print_R($fopts);exit;
153         if (!empty($fopts['DB_DataObject_translator'])) {
154             $tmp_opts['DB_DataObject_translator'] = $fopts['DB_DataObject_translator'];
155         }
156         if (!empty($fopts['locale'])) {
157             $tmp_opts['locale'] = $fopts['locale'];
158         }
159         if (!empty($fopts['templateDir'])) {
160             $tmp_opts['templateDir'] = $fopts['templateDir'];
161         }
162         // override.
163         if (!empty($this->templateDir)) {
164             $tmp_opts['templateDir'] = $this->templateDir;
165         }
166         
167         // local opt's overwrite
168         if (!empty($this->locale)) {
169             $tmp_opts['locale'] = $this->locale;
170         }
171         
172         $htmlbody = false;
173         $html_tmp_opts = $tmp_opts;
174         $htmltemplate = new HTML_Template_Flexy( $html_tmp_opts );
175         if (is_string($htmltemplate->resolvePath('mail/'.$templateFile.'.body.html')) ) { 
176             // then we have a multi-part email...
177             if (!empty($this->html_locale)) {
178                 $html_tmp_opts['locale'] = $this->html_locale;
179             }
180             $htmltemplate = new HTML_Template_Flexy( $html_tmp_opts );
181             
182             $htmltemplate->compile('mail/'. $templateFile.'.body.html');
183             $htmlbody =  $htmltemplate->bufferedOutputObject($content);
184             
185             $this->htmlbody = $htmlbody;
186             
187             // for the html body, we may want to convert the attachments to images.
188 //            var_dump($htmlbody);exit;
189             
190             if(!empty($content->body_cls) && strlen($content->body_cls)){
191                 $htmlbody = $this->htmlbodySetClass($htmlbody, $content->body_cls);
192             }
193             
194             if ($this->replaceImages) {
195                 $htmlbody = $this->htmlbodytoCID($htmlbody);    
196             }
197             
198             if ($this->css_embed) {
199                 $htmlbody = $this->htmlbodyCssEmbed($htmlbody);
200             }
201             
202             if ($this->css_inline && strlen($this->css_inline)) {
203                 $htmlbody = $this->htmlbodyInlineCss($htmlbody);
204             }
205             
206         }
207         $tmp_opts['nonHTML'] = true;
208         //$tmp_opts['debug'] = true;
209         
210         // print_R($tmp_opts);
211         // $tmp_opts['force'] = true;
212         
213         $template = new HTML_Template_Flexy(  $tmp_opts );
214         
215         $template->compile('mail/'. $templateFile.'.txt');
216         
217         /* use variables from this object to ouput data. */
218         $mailtext = $template->bufferedOutputObject($content);
219         //print_r($mailtext);exit;
220        
221         
222         
223         //echo "<PRE>";print_R($mailtext);
224         
225         /* With the output try and send an email, using a few tricks in Mail_MimeDecode. */
226         require_once 'Mail/mimeDecode.php';
227         require_once 'Mail.php';
228         
229         $decoder = new Mail_mimeDecode($mailtext);
230         $parts = $decoder->getSendArray();
231         if (PEAR::isError($parts)) {
232             return $parts;
233             //echo "PROBLEM: {$parts->message}";
234             //exit;
235         } 
236         
237         $isMime = false;
238         
239         require_once 'Mail/mime.php';
240         $mime = new Mail_mime(array(
241             'eol' => "\n",
242             //'html_encoding' => 'base64',
243             'html_charset' => 'utf-8',
244             'text_charset' => 'utf-8',
245             'head_charset' => 'utf-8',
246         ));
247         // clean up the headers...
248         
249         
250         $parts[1]['Message-Id'] = '<' .   $content->msgid   .
251                                      '@' . $content->HTTP_HOST .'>';
252         
253           
254         if ($htmlbody !== false) {
255             // got a html headers...
256             
257             if (isset($parts[1]['Content-Type'])) {
258                 unset($parts[1]['Content-Type']);
259             }
260             $mime->setTXTBody($parts[2]);
261             $this->textbody = $parts[2];
262             $mime->setHTMLBody($htmlbody);
263             
264 //            var_dump($mime);exit;
265             foreach($this->images as $cid=>$cdata) { 
266             
267                 $mime->addHTMLImage(
268                     $cdata['file'],
269                      $cdata['mimetype'],
270                      $cid.'.'.$cdata['ext'],
271                     true,
272                     $cdata['contentid']
273                 );
274             }
275             $isMime = true;
276         }
277         
278         if(!empty($this->attachments)){
279             //if got a attachments
280             $header = $mime->headers($parts[1]);
281             
282             if (isset($parts[1]['Content-Type'])) {
283                 unset($parts[1]['Content-Type']);
284             }
285             
286             if (!$isMime) {
287             
288                 if(preg_match('/text\/html/', $header['Content-Type'])){
289                     $mime->setHTMLBody($parts[2]);
290                     $mime->setTXTBody('This message is in HTML only');
291                     $this->textbody = 'This message is in HTML only';
292                 }else{
293                     $mime->setTXTBody($parts[2]);
294                     $this->textbody = $parts[2];
295                     $mime->setHTMLBody('<PRE>'.htmlspecialchars($parts[2]).'</PRE>');
296                 }
297             }
298             foreach($this->attachments as $attch){
299                 $mime->addAttachment(
300                         $attch['file'],
301                         $attch['mimetype'],
302                         (!empty($attch['name'])) ? $attch['name'] : '',
303                         true
304                 );
305             }
306             
307             $isMime = true;
308         }
309         
310         if($isMime){
311             $parts[2] = $mime->get();
312             $parts[1] = $mime->headers($parts[1]);
313         }
314          
315         
316         $ret = array(
317             'recipents' => $parts[0],
318             'headers' => $parts[1],
319             'body' => $parts[2],
320             'mailer' => $this
321         );
322         if ($this->rcpts !== false) {
323             $ret['recipents'] =  $this->rcpts;
324         }
325         // if 'to' is empty, then add the recipents in there... (must be an array?
326         if (!empty($ret['recipents']) && is_array($ret['recipents']) &&
327                 (empty($ret['headers']['To']) || !strlen(trim($ret['headers']['To'])))) {
328             $ret['headers']['To'] = implode(',', $ret['recipents']);
329         }
330        
331         
332         // add bcc if necessary..
333         if (!empty($this->bcc)) {
334            $ret['bcc'] = $this->bcc;
335         }
336         return $ret;
337     }
338     function send($email = false)
339     {
340                         
341         $ff = HTML_FlexyFramework::get();
342         
343         $pg = $ff->page;
344         
345         $email = is_array($email)  ? $email : $this->toData();
346         
347         if (is_a($email, 'PEAR_Error')) {
348             $pg->addEvent("COREMAILER-FAIL",  false, "email toData failed"); 
349       
350             
351             return $email;
352         }
353         
354         //$this->log( htmlspecialchars(print_r($email,true)));
355         
356         ///$recipents = array($this->email);
357 //        $mailOptions = PEAR::getStaticProperty('Mail','options');
358         
359         $mailOptions = isset($ff->Mail) ? $ff->Mail : array();
360         //print_R($mailOptions);exit;
361         
362         if ($this->mail_method == 'SMTPMX' && empty($mailOptions['mailname'])) {
363             $pg->jerr("Mail[mailname] is not set - this is required for SMTPMX");
364             
365         }
366         
367         $mail = Mail::factory($this->mail_method,$mailOptions);
368         if ($this->debug) {
369             $mail->debug = (bool) $this->debug;
370         }
371         
372         $email['headers']['Date'] = date('r'); 
373         if (PEAR::isError($mail)) {
374             $pg->addEvent("COREMAILER-FAIL",  false, "mail factory failed"); 
375       
376             
377             return $mail;
378         } 
379         $rcpts = $this->rcpts == false ? $email['recipents'] : $this->rcpts;
380         
381         
382         
383         // this makes contents untrustable...
384         if (!empty($this->contents['bcc']) && is_array($this->contents['bcc'])) {
385             $rcpts =array_merge(is_array($rcpts) ? $rcpts : array($rcpts), $this->contents['bcc']);
386         }
387         
388         $oe = error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
389         if ($this->debug) {
390             print_r(array(
391                 'rcpts' => $rcpts,
392                 'email' => $email
393             ));
394         }
395         $ret = $mail->send($rcpts,$email['headers'],$email['body']);
396         error_reporting($oe);
397         if ($ret === true) { 
398             $pg->addEvent("COREMAILER-SENT",  false,
399                 'To: ' .  ( is_array($rcpts) ? implode(', ', $rcpts) : $rcpts ) .
400                 'Subject: '  . @$email['headers']['Subject']
401             ); 
402         }  else {
403             $pg->addEvent("COREMAILER-FAIL",  false,
404                 "Sending to : " . ( is_array($rcpts) ? implode(', ', $rcpts) : $rcpts ) .
405                 " Error: " . $ret->toString());
406
407         }
408         
409         return $ret;
410     }
411     
412     function htmlbodytoCID($html)
413     {
414         $dom = new DOMDocument();
415         // this may raise parse errors as some html may be a component..
416         @$dom->loadHTML('<?xml encoding="UTF-8">' .$html);
417         $imgs= $dom->getElementsByTagName('img');
418         
419         $urls = array();
420         
421         foreach ($imgs as $i=>$img) {
422             $url  = $img->getAttribute('src');
423             if (preg_match('#^cid:#', $url)) {
424                 continue;
425             }
426             $me = $img->getAttribute('mailembed');
427             if ($me == 'no') {
428                 continue;
429             }
430             
431             if(!array_key_exists($url, $urls)){
432                 $conv = $this->fetchImage($url);
433                 $urls[$url] = $conv;
434                 $this->images[$conv['contentid']] = $conv;
435             } else {
436                 $conv = $urls[$url];
437             }
438             $img->setAttribute('origsrc', $url);
439             $img->setAttribute('src', 'cid:' . $conv['contentid']);
440         }
441         
442         
443         return $dom->saveHTML();
444         
445     }
446     function htmlbodyCssEmbed($html)
447     {
448         $ff = HTML_FlexyFramework::get();
449         $dom = new DOMDocument();
450         
451         // this may raise parse errors as some html may be a component..
452         @$dom->loadHTML('<?xml encoding="UTF-8">' .$html);
453         $links = $dom->getElementsByTagName('link');
454         $lc = array();
455         foreach ($links as $link) {  // duplicate as links is dynamic and we change it..!
456             $lc[] = $link;
457         }
458         //<link rel="stylesheet" type="text/css" href="{rootURL}/roojs1/css-mailer/mailer.css">
459         
460         foreach ($lc as $i=>$link) {
461             //var_dump($link->getAttribute('href'));
462             
463             if ($link->getAttribute('rel') != 'stylesheet') {
464                 continue;
465             }
466             $url  = $link->getAttribute('href');
467             $file = $ff->rootDir . $url;
468             
469             if (!preg_match('#^(http|https)://#', $url)) {
470                 $file = $ff->rootDir . $url;
471
472                 if (!file_exists($file)) {
473 //                    echo $file;
474                     $link->setAttribute('href', 'missing:' . $file);
475                     continue;
476                 }
477             } else {
478                $file = $this->mapurl($url);  
479             }
480             
481             $par = $link->parentNode;
482             $par->removeChild($link);
483             $s = $dom->createElement('style');
484             $e = $dom->createTextNode(file_get_contents($file));
485             $s->appendChild($e);
486             $par->appendChild($s);
487             
488         }
489         return $dom->saveHTML();
490         
491         
492     }
493     
494     function htmlbodyInlineCss($html)
495     {   
496         $dom = new DOMDocument();
497         
498         @$dom->loadHTML('<?xml encoding="UTF-8">' .$html);
499         
500         $html = $dom->getElementsByTagName('html');
501         $head = $dom->getElementsByTagName('head');
502         $body = $dom->getElementsByTagName('body');
503         
504         if(!$head->length){
505             $head = $dom->createElement('head');
506             $html->item(0)->insertBefore($head, $body->item(0));
507             $head = $dom->getElementsByTagName('head');
508         }
509         
510         $s = $dom->createElement('style');
511         $e = $dom->createTextNode($this->css_inline);
512         $s->appendChild($e);
513         $head->item(0)->appendChild($s);
514         
515         return $dom->saveHTML();
516         
517         /* Inline
518         require_once 'HTML/CSS/InlineStyle.php';
519         
520         $doc = new HTML_CSS_InlineStyle($html);
521         
522         $doc->applyStylesheet($this->css_inline);
523         
524         $html = $doc->getHTML();
525         
526         return $html;
527         */
528     }
529     
530     function htmlbodySetClass($html, $cls)
531     {
532         $dom = new DOMDocument();
533         
534         @$dom->loadHTML('<?xml encoding="UTF-8">' .$html);
535         
536         $body = $dom->getElementsByTagName('body');
537         if (!empty($body->length)) {
538             $body->item(0)->setAttribute('class', $cls);
539         } else {
540             $body = $dom->createElement("body");
541             $body->setAttribute('class', $cls);
542             $dom->appendChild($body);
543         }
544         
545         
546         return $dom->saveHTML();
547     }
548     
549     function fetchImage($url)
550     {
551         
552         
553         $this->log( "FETCH : $url\n");
554         
555         if ($url[0] == '/') {
556             $ff = HTML_FlexyFramework::get();
557             $file = $ff->rootDir . $url;
558             require_once 'File/MimeType.php';
559             $m  = new File_MimeType();
560             $mt = $m->fromFilename($file);
561             $ext = $m->toExt($mt); 
562             
563             return array(
564                     'mimetype' => $mt,
565                    'ext' => $ext,
566                    'contentid' => md5($file),  // mailer makes md5 cid's' -- cid with attachment-** are done by mailer.
567                    'file' => $file
568             );
569             
570             
571             
572         }
573         
574         //print_R($url); exit;
575         
576         
577         if (preg_match('#^file:///#', $url)) {
578             $file = preg_replace('#^file://#', '', $url);
579             require_once 'File/MimeType.php';
580             $m  = new File_MimeType();
581             $mt = $m->fromFilename($file);
582             $ext = $m->toExt($mt); 
583             
584             return array(
585                 'mimetype'  => $mt,
586                 'ext'       =>   $ext,
587                 'contentid' => md5($file),
588                 'file'      => $file
589             );
590             
591         }
592         
593         // CACHE???
594         // 2 files --- the info file.. and the actual file...
595         // add user
596         // unix only...
597         $uinfo = posix_getpwuid( posix_getuid () ); 
598         $user = $uinfo['name']; 
599         
600         $cache = ini_get('session.save_path')."/Pman_Core_Mailer-{$user}/" . md5($url);
601         if ($this->cache_images &&
602                 file_exists($cache) &&
603                 filemtime($cache) > strtotime('NOW - 1 WEEK')
604             ) {
605             $ret =  json_decode(file_get_contents($cache), true);
606             $this->log("fetched from cache");
607             $ret['file'] = $cache . '.data';
608             return $ret;
609         }
610         if (!file_exists(dirname($cache))) {
611             mkdir(dirname($cache),0700, true);
612         }
613         
614         require_once 'HTTP/Request.php';
615         
616         $real_url = str_replace(' ', '%20', $this->mapurl($url));
617         $a = new HTTP_Request($real_url);
618         $a->sendRequest();
619         $data = $a->getResponseBody();
620         
621         $this->log("got file of size " . strlen($data));
622         $this->log("save contentid " . md5($url));
623         
624         file_put_contents($cache .'.data', $data);
625         
626         
627         $mt = $a->getResponseHeader('Content-Type');
628         
629         require_once 'File/MimeType.php';
630         $m  = new File_MimeType();
631         $ext = $m->toExt($mt);
632         
633         $ret = array(
634             'mimetype' => $mt,
635             'ext' => $ext,
636             'contentid' => md5($url)
637             
638         );
639         
640         file_put_contents($cache, json_encode($ret));
641         $ret['file'] = $cache . '.data';
642         return $ret;
643     }  
644     
645     function mapurl($in)
646     {
647          foreach($this->urlmap as $o=>$n) {
648             if (strpos($in,$o) === 0) {
649                 $ret =$n . substr($in,strlen($o));
650                 $this->log("mapURL in $in = $ret");
651                 return $ret;
652             }
653         }
654         $this->log("mapurl no change - $in");
655         return $in;
656          
657         
658     }
659  
660     
661     
662     function log($val)
663     {
664         if (!$this->debug) {
665             return;
666         }
667         if ($this->debug < 2) {
668             echo '<PRE>' . print_r($val,true). "\n"; 
669             return;
670         }
671         $fh = fopen('/tmp/core_mailer.log', 'a');
672         fwrite($fh, date('Y-m-d H:i:s -') . json_encode($val) . "\n");
673         fclose($fh);
674         
675         
676     }
677     
678 }