Make CSS inline in HTML mails
[bdrem.git] / src / bdrem / Renderer / Mail.php
1 <?php
2 /**
3  * Part of bdrem
4  *
5  * PHP version 5
6  *
7  * @category  Tools
8  * @package   Bdrem
9  * @author    Christian Weiske <cweiske@cweiske.de>
10  * @copyright 2014 Christian Weiske
11  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
12  * @link      http://cweiske.de/bdrem.htm
13  */
14 namespace bdrem;
15
16 require_once 'Mail/mime.php';
17
18 /**
19  * Send out mails
20  *
21  * @category  Tools
22  * @package   Bdrem
23  * @author    Christian Weiske <cweiske@cweiske.de>
24  * @copyright 2014 Christian Weiske
25  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
26  * @version   Release: @package_version@
27  * @link      http://cweiske.de/bdrem.htm
28  */
29 class Renderer_Mail extends Renderer
30 {
31     /**
32      * Add HTML part to email
33      * @var bool
34      */
35     public $html = true;
36
37     /**
38      * CSS "inline" in tags, or "separate" in a style block
39      * @var string
40      */
41     public $css = 'inline';
42
43     /**
44      * Render the events - send out mails.
45      *
46      * Uses the config's "mail_to" array as recipients.
47      * Sends out a single mail for each recipient.
48      * Config "mail_from" can also be used.
49      *
50      * @param array $arEvents Array of events to display
51      *
52      * @return void
53      */
54     public function render($arEvents)
55     {
56         $todays = array();
57         foreach ($arEvents as $event) {
58             if ($event->days == 0) {
59                 $todays[] = $this->shorten($event->title, 15);
60             }
61         }
62         $subject = 'Birthday reminder';
63         if (count($todays)) {
64             $subject .= ': ' . implode(', ', $todays);
65         }
66
67         $rc  = new Renderer_Console();
68         $rht = new Renderer_HtmlTable();
69
70         $hdrs = array(
71             'From'    => $this->config->get('mail_from', 'birthday@example.org'),
72             'Auto-Submitted' => 'auto-generated'
73         );
74         $mime = new \Mail_mime(
75             array(
76                 'eol' => "\n",
77                 'head_charset' => 'utf-8',
78                 'text_charset' => 'utf-8',
79                 'html_charset' => 'utf-8',
80             )
81         );
82
83         $mime->setTXTBody($rc->render($arEvents));
84         if ($this->html) {
85             if ($this->css == 'inline') {
86                 $html = $this->inlineCss(
87                     $rht->render($arEvents),
88                     Renderer_Html::getCss()
89                 );
90             } else {
91                 $html = '<style type="text/css">'
92                     . Renderer_Html::getCss()
93                     . '</style>'
94                     . $rht->render($arEvents);
95             }
96             $mime->setHTMLBody($this->minifyHtml($html));
97         }
98
99         $body = $mime->get();
100         $hdrs = $mime->headers($hdrs);
101         $textHeaders = '';
102         foreach ($hdrs as $k => $v) {
103             $textHeaders .= $k . ': ' . $v  . "\n";
104         }
105
106         if (!$this->config->get('debug', false)) {
107             foreach ((array) $this->config->get('mail_to') as $recipient) {
108                 mail($recipient, $subject, $body, $textHeaders);
109             }
110         } else {
111             echo "Subject: " . $subject . "\n";
112             echo $textHeaders;
113             echo "\n";
114             echo $body;
115         }
116     }
117
118     /**
119      * Shorten the given string to the specified length.
120      * Adds ... when the string was too long
121      *
122      * @param string  $str String to shorten
123      * @param integer $len Maximum length of the string
124      *
125      * @return string Shortened string
126      */
127     protected function shorten($str, $len)
128     {
129         if (mb_strlen($str) <= $len) {
130             return $str;
131         }
132
133         return mb_substr($str, 0, $len - 1) . '…';
134     }
135
136     /**
137      * Takes the HTML and CSS code and inlines CSS into HTML.
138      *
139      * This is important for some e-mail clients which do
140      * not interpret <style> tags but only support inline styles.
141      *
142      * Works nicely with bdrem's CSS. If you need more CSS selector
143      * support, have a look at https://github.com/jjriv/emogrifier
144      *
145      * @param string $html HTML code
146      * @param string $css  CSS code
147      *
148      * @return string HTML with inlined CSS
149      */
150     protected function inlineCss($html, $css)
151     {
152         preg_match_all(
153             '#([^{]+) {([^}]+)}#m',
154             $css,
155             $parts
156         );
157         $rules = array();
158         foreach ($parts[1] as $key => $rule) {
159             $mrules = explode(',', $rule);
160             foreach ($mrules as $rule) {
161                 $rule  = trim($rule);
162                 $style = trim($parts[2][$key]);
163                 $rules[$rule] = preg_replace(
164                     '#([:;]) +#', '\1',
165                     str_replace(
166                         ["\r", "\n", '    '],
167                         ['', '', ' '],
168                         $style
169                     )
170                 );
171             }
172         }
173         $sx = simplexml_load_string($html);
174         foreach ($rules as $rule => $style) {
175             $mode = null;
176             $parts = explode(' ', $rule);
177             $xp = '';
178             foreach ($parts as $part) {
179                 $part = trim($part);
180                 if (strpos($part, ':') !== false) {
181                     //.foo:before
182                     list($part, $mode) = explode(':', $part);
183                     if ($mode == 'hover') {
184                         continue 2;
185                     }
186                 }
187                 if (strpos($part, '.') === false) {
188                     //tag only
189                     if ($part == '') {
190                         $xp = '//*';
191                     } else {
192                         $xp .= '//' . $part;
193                     }
194                 } else {
195                     //tag.class
196                     list($tag, $class) = explode('.', $part);
197                     if ($tag == '') {
198                         $tag = '*';
199                     }
200                     $xp .= '//' . $tag
201                         . '[contains('
202                         . 'concat(" ", normalize-space(@class), " "), '
203                         . '" ' . $class . ' "'
204                         . ')]';
205                 }
206             }
207             $res = $sx->xpath($xp);
208             //var_dump($res);die();
209             //var_dump($xp, $style);
210             foreach ($res as $xelem) {
211                 if ($mode === null) {
212                     $xelem['style'] .= $style;
213                 } else if ($mode == 'before') {
214                     $xelem[0] = preg_replace(
215                         '#content:\s*"(.+)"#', '\1', $style
216                     );
217                 }
218             }
219         }
220
221         $html = $sx->asXML();
222         //strip xml header
223         $lines = explode("\n", $html);
224         unset($lines[0]);
225         $html = implode("\n", $lines);
226
227         //echo $html . "\n";die();
228         return $html;
229     }
230
231     /**
232      * Remove whitespace between tags
233      *
234      * @param string $html HTML code
235      *
236      * @return string Smaller HTML code
237      */
238     protected function minifyHtml($html)
239     {
240         $html = trim(preg_replace("#[\n\r ]+<#", '<', $html));
241         return $html;
242     }
243 }
244 ?>