PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/Classes/Mailer.php

https://github.com/marble/newsletter
PHP | 465 lines | 257 code | 63 blank | 145 comment | 22 complexity | 6f1d61a445dbe962335571788ac59e11 MD5 | raw file
  1. <?php
  2. /* * *************************************************************
  3. * Copyright notice
  4. *
  5. * (c) 2014
  6. * All rights reserved
  7. *
  8. * This script is part of the TYPO3 project. The TYPO3 project is
  9. * free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * The GNU General Public License can be found at
  15. * http://www.gnu.org/copyleft/gpl.html.
  16. *
  17. * This script is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU General Public License for more details.
  21. *
  22. * This copyright notice MUST APPEAR in all copies of the script!
  23. * ************************************************************* */
  24. require_once(PATH_typo3 . 'contrib/swiftmailer/swift_required.php');
  25. /**
  26. * This is the holy inner core of newsletter.
  27. * It is normally used in an instance per language to compile MIME 1.0 compatible mails
  28. *
  29. * @package Newsletter
  30. * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
  31. */
  32. class Tx_Newsletter_Mailer
  33. {
  34. /**
  35. * @var Tx_Newsletter_Domain_Model_Newsletter $newsletter
  36. */
  37. private $newsletter;
  38. private $html;
  39. private $html_tpl;
  40. private $title;
  41. private $title_tpl;
  42. private $senderName;
  43. private $senderEmail;
  44. private $bounceAddress;
  45. private $siteUrl;
  46. private $homeUrl;
  47. private $attachments = array();
  48. private $attachmentsEmbedded = array();
  49. private $linksCache = array();
  50. /**
  51. * Constructor that set up basic internal datastructures. Do not call directly
  52. *
  53. */
  54. public function __construct()
  55. {
  56. global $TYPO3_CONF_VARS;
  57. /* Read some basic settings */
  58. $this->extConf = unserialize($TYPO3_CONF_VARS['EXT']['extConf']['newsletter']);
  59. $this->realPath = PATH_site;
  60. }
  61. /**
  62. * Returns the current HTML content
  63. */
  64. public function getHtml()
  65. {
  66. return $this->html;
  67. }
  68. /**
  69. * Returns the current Plain Text content
  70. */
  71. public function getPlain()
  72. {
  73. $plainConverter = $this->newsletter->getPlainConverterInstance();
  74. $plainText = $plainConverter->getPlaintext($this->getHtml(), $this->domain);
  75. return $plainText;
  76. }
  77. public function setNewsletter(Tx_Newsletter_Domain_Model_Newsletter $newsletter, $language = null)
  78. {
  79. $domain = $newsletter->getDomain();
  80. // When sending newsletter via scheduler (so via CLI mode) realurl cannot guess
  81. // the domain name by himself, so we help him by filling HTTP_HOST variable
  82. $_SERVER['HTTP_HOST'] = $domain;
  83. $_SERVER['SCRIPT_NAME'] = '/index.php';
  84. $this->siteUrl = "http://$domain/";
  85. $this->linksCache = array();
  86. $this->newsletter = $newsletter;
  87. $this->homeUrl = $this->siteUrl . t3lib_extMgm::siteRelPath('newsletter');
  88. $this->senderName = $newsletter->getSenderName();
  89. $this->senderEmail = $newsletter->getSenderEmail();
  90. $bounceAccount = $newsletter->getBounceAccount();
  91. $this->bounceAddress = $bounceAccount ? $bounceAccount->getEmail() : '';
  92. // Build html
  93. $validatedContent = $newsletter->getValidatedContent($language);
  94. if (count($validatedContent['errors'])) {
  95. throw new Exception('The newsletter HTML content does not validate. The sending is aborted. See errors: ' . serialize($validatedContent['errors']));
  96. }
  97. $this->setHtml($validatedContent['content']);
  98. // Build title from HTML source (we cannot use $newsletter->getTitle(), because it is NOT localized)
  99. $this->setTitle($validatedContent['content']);
  100. // Attaching files
  101. $files = $newsletter->getAttachments();
  102. foreach ($files as $file) {
  103. if (trim($file) != '') {
  104. $filename = PATH_site . "uploads/tx_newsletter/$file";
  105. $this->attachments[] = Swift_Attachment::fromPath($filename);
  106. }
  107. }
  108. }
  109. /**
  110. * Extract the title from <title></title> HTML tags
  111. * @param string $htmlSrc
  112. */
  113. private function setTitle($htmlSrc)
  114. {
  115. // Extract title from HTML
  116. preg_match('|<title[^>]*>(.*)</title>|i', $htmlSrc, $m);
  117. $title = trim($m[1]);
  118. /* Detect what markers we need to substitute later on */
  119. preg_match_all('/###[\w]+###/', $title, $fields);
  120. $this->titleMarkers = str_replace('###', '', $fields[0]);
  121. /* Any advanced markers we need to sustitute later on */
  122. $this->titleAdvancedMarkers = array();
  123. preg_match_all('/###:IF: (\w+) ###/U', $title, $fields);
  124. foreach ($fields[1] as $field) {
  125. $this->titleAdvancedMarkers[] = $field;
  126. }
  127. $this->title_tpl = $title;
  128. $this->title = $title;
  129. }
  130. /**
  131. * Set the html content of the mail which will be used as template.
  132. * The content will be edited to include images as attachements if needed.
  133. *
  134. * @param string The html content of the mail
  135. * @return void
  136. */
  137. private function setHtml($src)
  138. {
  139. // Convert external files resources to attached files or correct their links
  140. $replace_regs = array(
  141. '/ src="([^"]+)"/',
  142. '/ background="([^"]+)"/',
  143. );
  144. // Attach images if option is set
  145. if ($this->extConf['attach_images']) {
  146. foreach ($replace_regs as $replace_reg) {
  147. preg_match_all($replace_reg, $src, $urls);
  148. foreach ($urls[1] as $i => $url) {
  149. // Mark places for embedded files and keep the embed files to be replaced
  150. $swiftEmbeddedMarker = '###_#_SWIFT_EMBEDDED_MARKER_' . count($this->attachmentsEmbedded) . '_#_###';
  151. $this->attachmentsEmbedded[$swiftEmbeddedMarker] = Swift_EmbeddedFile::fromPath($url);
  152. $src = str_replace($urls[0][$i], str_replace($url, $swiftEmbeddedMarker, $urls[0][$i]), $src);
  153. }
  154. }
  155. }
  156. // Detect what markers we need to substitute later on
  157. preg_match_all('/###(\w+)###/', $src, $fields);
  158. preg_match_all('|"http://(\w+)"|', $src, $fieldsLinks);
  159. $this->htmlMarkers = array_merge($fields[1], $fieldsLinks[1]);
  160. // Any advanced IF fields we need to sustitute later on
  161. $this->htmlAdvancedMarkers = array();
  162. preg_match_all('/###:IF: (\w+) ###/U', $src, $fields);
  163. foreach ($fields[1] as $field) {
  164. $this->htmlAdvancedMarkers[] = $field;
  165. }
  166. $this->html_tpl = $src;
  167. $this->html = $src;
  168. }
  169. /**
  170. * Insert a "mail-open-spy" in the mail.
  171. *
  172. * @return void
  173. */
  174. private function injectOpenSpy(Tx_Newsletter_Domain_Model_Email $email)
  175. {
  176. $this->html = str_ireplace(
  177. '</body>', '<div><img src="' . Tx_Newsletter_Tools::buildFrontendUri('opened', array(), 'Email') . '&c=' . $email->getAuthCode() . '" width="0" height="0" /></div></body>', $this->html);
  178. }
  179. /**
  180. * Reset all modifications to the content.
  181. *
  182. * @return void
  183. */
  184. private function resetMarkers()
  185. {
  186. $this->html = $this->html_tpl;
  187. $this->title = $this->title_tpl;
  188. }
  189. /**
  190. * Replace a named marker with a suppied value.
  191. * A marker can have the form of a simple string marker ###marker###, or http://marker
  192. * Or an advanced conditionnal marker ###:IF: marker ### ..content.. (###:ELSE:###)? ..content.. ###:ENDIF:###
  193. *
  194. * @param string Name of the marker to replace
  195. * @param string Value to replace marker with.
  196. * @return void
  197. */
  198. private function substituteMarker($name, $value)
  199. {
  200. // For each marker, only substitute if the field is registered as a marker.
  201. // This approach has shown to speed up things quite a bit.
  202. if (in_array($name, $this->htmlAdvancedMarkers)) {
  203. $this->html = self::advancedSubstituteMarker($this->html, $name, $value);
  204. }
  205. if (in_array($name, $this->titleAdvancedMarkers)) {
  206. $this->title = self::advancedSubstituteMarker($this->title, $name, $value);
  207. }
  208. // All variants of the marker to search
  209. $search = array(
  210. "###$name###",
  211. "http://$name",
  212. urlencode("http://$name"), // If the marker is in a link and the "links spy" option is activated it will be urlencoded
  213. );
  214. $replace = array(
  215. $value,
  216. $value,
  217. urlencode($value), // We need to replace with urlencoded value
  218. );
  219. if (in_array($name, $this->htmlMarkers)) {
  220. $this->html = str_ireplace($search, $replace, $this->html);
  221. }
  222. if (in_array($name, $this->titleMarkers)) {
  223. $this->title = str_ireplace($search, $replace, $this->title);
  224. }
  225. }
  226. /**
  227. * Substitute an advanced marker.
  228. *
  229. * @internal
  230. * @param string Source to apply marker substitution to.
  231. * @param string Name of marker.
  232. * @param boolean Display value of marker.
  233. * @return string Source with applied marker.
  234. */
  235. private function advancedSubstituteMarker($src, $name, $value)
  236. {
  237. preg_match_all("/###:IF: $name ###([\w\W]*)###:ELSE:###([\w\W]*)###:ENDIF:###/U", $src, $matches);
  238. foreach ($matches[0] as $i => $full_mark) {
  239. if ($value) {
  240. $src = str_ireplace($full_mark, $matches[1][$i], $src);
  241. } else {
  242. $src = str_ireplace($full_mark, $matches[2][$i], $src);
  243. }
  244. }
  245. preg_match_all("/###:IF: $name ###([\w\W]*)###:ENDIF:###/U", $src, $matches);
  246. foreach ($matches[0] as $i => $full_mark) {
  247. if ($value) {
  248. $src = str_ireplace($full_mark, $matches[1][$i], $src);
  249. } else {
  250. $src = str_ireplace($full_mark, '', $src);
  251. }
  252. }
  253. return $src;
  254. }
  255. /**
  256. * Apply multiple markers to mail contents
  257. *
  258. * @param array Assoc array with name => value pairs.
  259. * @return void
  260. */
  261. private function substituteMarkers(Tx_Newsletter_Domain_Model_Email $email)
  262. {
  263. $markers = $email->getRecipientData();
  264. // Add predefined markers
  265. $authCode = $email->getAuthCode();
  266. $markers['newsletter_view_url'] = Tx_Newsletter_Tools::buildFrontendUri('show', array(), 'Email') . '&c=' . $authCode;
  267. $markers['newsletter_unsubscribe_url'] = Tx_Newsletter_Tools::buildFrontendUri('unsubscribe', array(), 'Email') . '&c=' . $authCode;
  268. if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['newsletter']['substituteMarkersHook'])) {
  269. foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['newsletter']['substituteMarkersHook'] as $_classRef) {
  270. $_procObj = & t3lib_div::getUserObj($_classRef);
  271. $this->html = $_procObj->substituteMarkersHook($this->html, 'html', $markers);
  272. $this->title = $_procObj->substituteMarkersHook($this->title, 'title', $markers);
  273. }
  274. }
  275. foreach ($markers as $name => $value) {
  276. $this->substituteMarker($name, $value);
  277. }
  278. }
  279. private function getLinkAuthCode(Tx_Newsletter_Domain_Model_Email $email, $url, $isPreview, $isPlainText = false)
  280. {
  281. global $TYPO3_DB;
  282. $url = html_entity_decode($url);
  283. // First check in our local cache
  284. if (isset($this->linksCache[$url])) {
  285. $linkId = $this->linksCache[$url];
  286. }
  287. // Otherwise if we are preparing a preview, just generate incremental ID and do not touch database at all
  288. elseif ($isPreview) {
  289. $linkId = count($this->linksCache);
  290. }
  291. // Finally if we it's not a preview and link was not in cache, check database
  292. else {
  293. // Look for the link database, it may already exist
  294. $res = $TYPO3_DB->sql_query('SELECT uid FROM tx_newsletter_domain_model_link WHERE url = "' . $url . '" AND newsletter = ' . $this->newsletter->getUid() . ' LIMIT 1');
  295. $row = $TYPO3_DB->sql_fetch_row($res);
  296. if ($row) {
  297. $linkId = $row[0];
  298. }
  299. // Otherwise create it
  300. else {
  301. $TYPO3_DB->exec_INSERTquery('tx_newsletter_domain_model_link', array(
  302. 'pid' => $this->newsletter->getPid(),
  303. 'url' => $url,
  304. 'newsletter' => $this->newsletter->getUid(),
  305. ));
  306. $linkId = $TYPO3_DB->sql_insert_id();
  307. }
  308. }
  309. // Store link in cache
  310. $this->linksCache[$url] = $linkId;
  311. $authCode = md5($email->getAuthCode() . $linkId);
  312. $newUrl = Tx_Newsletter_Tools::buildFrontendUri('clicked', array(), 'Link') . '&url=' . urlencode($url) . '&n=' . $this->newsletter->getUid() . '&l=' . $authCode . ($isPlainText ? '&p=1' : '');
  313. return $newUrl;
  314. }
  315. /**
  316. * Replace all links in the mail to make spy links.
  317. *
  318. * @param Tx_Newsletter_Domain_Model_Email $email The email to prepare the newsletter for
  319. * @param boolean $isPreview whether we are preparing a preview version (if true links will not be stored in database thus no statistics will be available)
  320. * @return void
  321. */
  322. private function injectLinksSpy(Tx_Newsletter_Domain_Model_Email $email, $isPreview)
  323. {
  324. /* Exchange all http:// links html */
  325. preg_match_all('|<a [^>]*href="(https?://[^"]*)"|Ui', $this->html, $urls);
  326. foreach ($urls[1] as $i => $url) {
  327. $newUrl = $this->getLinkAuthCode($email, $url, $isPreview);
  328. /* Two step replace to be as precise as possible */
  329. $link = str_replace($url, $newUrl, $urls[0][$i]);
  330. $this->html = str_replace($urls[0][$i], $link, $this->html);
  331. }
  332. }
  333. /**
  334. * Prepare the newsletter content for the specified email (substitute markers and insert spies)
  335. * @param Tx_Newsletter_Domain_Model_Email $email
  336. * @param boolean $isPreview whether we are preparing a preview version of the newsletter
  337. */
  338. public function prepare(Tx_Newsletter_Domain_Model_Email $email, $isPreview = false)
  339. {
  340. $this->resetMarkers();
  341. if ($this->newsletter->getInjectOpenSpy())
  342. $this->injectOpenSpy($email);
  343. if ($this->newsletter->getInjectLinksSpy())
  344. $this->injectLinksSpy($email, $isPreview);
  345. // We substitute markers last because we don't want to spy each links to view/unsubscribe
  346. // (created via markers) for each recipient. Only the generic marker is enough.
  347. // Otherwise we would mess up opened link statistics
  348. $this->substituteMarkers($email);
  349. }
  350. /**
  351. * The regular send method. Use this to send a normal, personalized mail.
  352. *
  353. * @param Tx_Newsletter_Domain_Model_Email $email The email object containing recipient email address and extra data for markers
  354. * @return void
  355. */
  356. public function send(Tx_Newsletter_Domain_Model_Email $email)
  357. {
  358. $this->prepare($email);
  359. $this->raw_send($email);
  360. }
  361. /**
  362. * Raw send method. This does not replace markers, or reset the mail afterwards.
  363. *
  364. * @interal
  365. * @param array Record with receivers information as name => value pairs.
  366. * @param array Array with extra headers to apply to mails as name => value pairs.
  367. * @return void
  368. */
  369. private function raw_send(Tx_Newsletter_Domain_Model_Email $email)
  370. {
  371. $message = t3lib_div::makeInstance('t3lib_mail_Message');
  372. $message->setTo($email->getRecipientAddress())
  373. ->setFrom(array($this->senderEmail => $this->senderName))
  374. ->setSubject($this->title)
  375. ;
  376. if ($this->bounceAddress) {
  377. $message->setReturnPath($this->bounceAddress);
  378. }
  379. foreach ($this->attachments as $attachment) {
  380. $message->attach($attachment);
  381. }
  382. // Specify message-id for bounce identification
  383. $msgId = $message->getHeaders()->get('Message-ID');
  384. $msgId->setId($email->getAuthCode() . '@' . $this->newsletter->getDomain());
  385. // Build plaintext
  386. $plain = $this->getPlain();
  387. $recipientData = $email->getRecipientData();
  388. if ($recipientData['plain_only']) {
  389. $message->setBody($plain, 'text/plain');
  390. } else {
  391. // Attach inline files and replace markers used for URL
  392. foreach ($this->attachmentsEmbedded as $marker => $attachment) {
  393. $embeddedSrc = $message->embed($attachment);
  394. $plain = str_replace($marker, $embeddedSrc, $plain);
  395. $this->html = str_replace($marker, $embeddedSrc, $this->html);
  396. }
  397. $message->setBody($this->html, 'text/html');
  398. $message->addPart($plain, 'text/plain');
  399. }
  400. $message->send();
  401. }
  402. }