smtp.mail.inc 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. <?php
  2. /**
  3. * @file
  4. * The code processing mail in the smtp module.
  5. *
  6. */
  7. /**
  8. * Modify the drupal mail system to use smtp when sending emails.
  9. * Include the option to choose between plain text or HTML
  10. */
  11. class SmtpMailSystem implements MailSystemInterface {
  12. protected $AllowHtml;
  13. /**
  14. * Concatenate and wrap the e-mail body for either
  15. * plain-text or HTML emails.
  16. *
  17. * @param $message
  18. * A message array, as described in hook_mail_alter().
  19. *
  20. * @return
  21. * The formatted $message.
  22. */
  23. public function format(array $message) {
  24. $this->AllowHtml = variable_get('smtp_allowhtml', 0);
  25. // Join the body array into one string.
  26. $message['body'] = implode("\n\n", $message['body']);
  27. if ($this->AllowHtml == 0) {
  28. // Convert any HTML to plain-text.
  29. $message['body'] = drupal_html_to_text($message['body']);
  30. // Wrap the mail body for sending.
  31. $message['body'] = drupal_wrap_mail($message['body']);
  32. }
  33. return $message;
  34. }
  35. /**
  36. * Send the e-mail message.
  37. *
  38. * @see drupal_mail()
  39. *
  40. * @param $message
  41. * A message array, as described in hook_mail_alter().
  42. * @return
  43. * TRUE if the mail was successfully accepted, otherwise FALSE.
  44. */
  45. public function mail(array $message) {
  46. $id = $message['id'];
  47. $to = $message['to'];
  48. $from = $message['from'];
  49. $body = $message['body'];
  50. $headers = $message['headers'];
  51. $subject = $message['subject'];
  52. // Create a new PHPMailer object - autoloaded from registry.
  53. $mailer = new PHPMailer();
  54. // Turn on debugging, if requested.
  55. if (variable_get('smtp_debugging', 0) == 1) {
  56. $mailer->SMTPDebug = TRUE;
  57. }
  58. // Set the from name.
  59. if (variable_get('smtp_fromname', '') != '') {
  60. $from_name = variable_get('smtp_fromname', '');
  61. }
  62. else {
  63. // If value is not defined in settings, use site_name.
  64. $from_name = variable_get('site_name', '');
  65. }
  66. //Hack to fix reply-to issue.
  67. $properfrom = variable_get('site_mail', '');
  68. if (!empty($properfrom)) {
  69. $headers['From'] = $properfrom;
  70. }
  71. if (!isset($headers['Reply-To']) || empty($headers['Reply-To'])) {
  72. if (strpos($from, '<')) {
  73. $reply = preg_replace('/>.*/', '', preg_replace('/.*</', '', $from));
  74. }
  75. else {
  76. $reply = $from;
  77. }
  78. $headers['Reply-To'] = $reply;
  79. }
  80. // Blank value will let the e-mail address appear.
  81. if ($from == NULL || $from == '') {
  82. // If from e-mail address is blank, use smtp_from config option.
  83. if (($from = variable_get('smtp_from', '')) == '') {
  84. // If smtp_from config option is blank, use site_email.
  85. if (($from = variable_get('site_mail', '')) == '') {
  86. drupal_set_message(t('There is no submitted from address.'), 'error');
  87. watchdog('smtp', 'There is no submitted from address.', array(), WATCHDOG_ERROR);
  88. return FALSE;
  89. }
  90. }
  91. }
  92. if (preg_match('/^"?.*"?\s*<.*>$/', $from)) {
  93. // . == Matches any single character except line break characters \r and \n.
  94. // * == Repeats the previous item zero or more times.
  95. $from_name = preg_replace('/"?([^("\t\n)]*)"?.*$/', '$1', $from); // It gives: Name
  96. $from = preg_replace("/(.*)\<(.*)\>/i", '$2', $from); // It gives: name@domain.tld
  97. }
  98. elseif (!valid_email_address($from)) {
  99. drupal_set_message(t('The submitted from address (@from) is not valid.', array('@from' => $from)), 'error');
  100. watchdog('smtp', 'The submitted from address (@from) is not valid.', array('@from' => $from), WATCHDOG_ERROR);
  101. return FALSE;
  102. }
  103. // Defines the From value to what we expect.
  104. $mailer->From = $from;
  105. $mailer->FromName = $from_name;
  106. $mailer->Sender = $from;
  107. // Create the list of 'To:' recipients.
  108. $torecipients = explode(',', $to);
  109. foreach ($torecipients as $torecipient) {
  110. if (strpos($torecipient, '<') !== FALSE) {
  111. $toparts = explode(' <', $torecipient);
  112. $toname = $toparts[0];
  113. $toaddr = rtrim($toparts[1], '>');
  114. }
  115. else {
  116. $toname = '';
  117. $toaddr = $torecipient;
  118. }
  119. $mailer->AddAddress($toaddr, $toname);
  120. }
  121. // Parse the headers of the message and set the PHPMailer object's settings
  122. // accordingly.
  123. foreach ($headers as $key => $value) {
  124. //watchdog('error', 'Key: ' . $key . ' Value: ' . $value);
  125. switch (drupal_strtolower($key)) {
  126. case 'from':
  127. if ($from == NULL or $from == '') {
  128. // If a from value was already given, then set based on header.
  129. // Should be the most common situation since drupal_mail moves the
  130. // from to headers.
  131. $from = $value;
  132. $mailer->From = $value;
  133. // then from can be out of sync with from_name !
  134. $mailer->FromName = '';
  135. $mailer->Sender = $value;
  136. }
  137. break;
  138. case 'content-type':
  139. // Parse several values on the Content-type header, storing them in an array like
  140. // key=value -> $vars['key']='value'
  141. $vars = explode(';', $value);
  142. foreach ($vars as $i => $var) {
  143. if ($cut = strpos($var, '=')) {
  144. $new_var = trim(drupal_strtolower(drupal_substr($var, $cut + 1)));
  145. $new_key = trim(drupal_substr($var, 0, $cut));
  146. unset($vars[$i]);
  147. $vars[$new_key] = $new_var;
  148. }
  149. }
  150. // Set the charset based on the provided value, otherwise set it to UTF-8 (which is Drupals internal default).
  151. $mailer->CharSet = isset($vars['charset']) ? $vars['charset'] : 'UTF-8';
  152. // If $vars is empty then set an empty value at index 0 to avoid a PHP warning in the next statement
  153. $vars[0] = isset($vars[0])?$vars[0]:'';
  154. switch ($vars[0]) {
  155. case 'text/plain':
  156. // The message includes only a plain text part.
  157. $mailer->IsHTML(FALSE);
  158. $content_type = 'text/plain';
  159. break;
  160. case 'text/html':
  161. // The message includes only an HTML part.
  162. $mailer->IsHTML(TRUE);
  163. $content_type = 'text/html';
  164. break;
  165. case 'multipart/related':
  166. // Get the boundary ID from the Content-Type header.
  167. $boundary = $this->_get_substring($value, 'boundary', '"', '"');
  168. // The message includes an HTML part w/inline attachments.
  169. $mailer->ContentType = $content_type = 'multipart/related; boundary="' . $boundary . '"';
  170. break;
  171. case 'multipart/alternative':
  172. // The message includes both a plain text and an HTML part.
  173. $mailer->ContentType = $content_type = 'multipart/alternative';
  174. // Get the boundary ID from the Content-Type header.
  175. $boundary = $this->_get_substring($value, 'boundary', '"', '"');
  176. break;
  177. case 'multipart/mixed':
  178. // The message includes one or more attachments.
  179. $mailer->ContentType = $content_type = 'multipart/mixed';
  180. // Get the boundary ID from the Content-Type header.
  181. $boundary = $this->_get_substring($value, 'boundary', '"', '"');
  182. break;
  183. default:
  184. // Everything else is unsuppored by PHPMailer.
  185. drupal_set_message(t('The %header of your message is not supported by PHPMailer and will be sent as text/plain instead.', array('%header' => "Content-Type: $value")), 'error');
  186. watchdog('smtp', 'The %header of your message is not supported by PHPMailer and will be sent as text/plain instead.', array('%header' => "Content-Type: $value"), WATCHDOG_ERROR);
  187. // Force the Content-Type to be text/plain.
  188. $mailer->IsHTML(FALSE);
  189. $content_type = 'text/plain';
  190. }
  191. break;
  192. case 'reply-to':
  193. // Only add a "reply-to" if it's not the same as "return-path".
  194. if ($value != $headers['Return-Path']) {
  195. if (strpos($value, '<') !== FALSE) {
  196. $replyToParts = explode('<', $value);
  197. $replyToName = trim($replyToParts[0]);
  198. $replyToName = trim($replyToName, '"');
  199. $replyToAddr = rtrim($replyToParts[1], '>');
  200. $mailer->AddReplyTo($replyToAddr, $replyToName);
  201. }
  202. else {
  203. $mailer->AddReplyTo($value);
  204. }
  205. }
  206. break;
  207. case 'content-transfer-encoding':
  208. $mailer->Encoding = $value;
  209. break;
  210. case 'return-path':
  211. case 'mime-version':
  212. case 'x-mailer':
  213. // Let PHPMailer specify these.
  214. break;
  215. case 'errors-to':
  216. $mailer->AddCustomHeader('Errors-To: ' . $value);
  217. break;
  218. case 'cc':
  219. $ccrecipients = explode(',', $value);
  220. foreach ($ccrecipients as $ccrecipient) {
  221. if (strpos($ccrecipient, '<') !== FALSE) {
  222. $ccparts = explode(' <', $ccrecipient);
  223. $ccname = $ccparts[0];
  224. $ccaddr = rtrim($ccparts[1], '>');
  225. }
  226. else {
  227. $ccname = '';
  228. $ccaddr = $ccrecipient;
  229. }
  230. $mailer->AddCC($ccaddr, $ccname);
  231. }
  232. break;
  233. case 'bcc':
  234. $bccrecipients = explode(',', $value);
  235. foreach ($bccrecipients as $bccrecipient) {
  236. if (strpos($bccrecipient, '<') !== FALSE) {
  237. $bccparts = explode(' <', $bccrecipient);
  238. $bccname = $bccparts[0];
  239. $bccaddr = rtrim($bccparts[1], '>');
  240. }
  241. else {
  242. $bccname = '';
  243. $bccaddr = $bccrecipient;
  244. }
  245. $mailer->AddBCC($bccaddr, $bccname);
  246. }
  247. break;
  248. default:
  249. // The header key is not special - add it as is.
  250. $mailer->AddCustomHeader($key . ': ' . $value);
  251. }
  252. }
  253. /**
  254. * TODO
  255. * Need to figure out the following.
  256. *
  257. * Add one last header item, but not if it has already been added.
  258. * $errors_to = FALSE;
  259. * foreach ($mailer->CustomHeader as $custom_header) {
  260. * if ($custom_header[0] = '') {
  261. * $errors_to = TRUE;
  262. * }
  263. * }
  264. * if ($errors_to) {
  265. * $mailer->AddCustomHeader('Errors-To: '. $from);
  266. * }
  267. */
  268. // Add the message's subject.
  269. $mailer->Subject = $subject;
  270. // Processes the message's body.
  271. switch ($content_type) {
  272. case 'multipart/related':
  273. $mailer->Body = $body;
  274. /**
  275. * TODO
  276. * Firgure out if there is anything more to handling this type.
  277. */
  278. break;
  279. case 'multipart/alternative':
  280. // Split the body based on the boundary ID.
  281. $body_parts = $this->_boundary_split($body, $boundary);
  282. foreach ($body_parts as $body_part) {
  283. // If plain/text within the body part, add it to $mailer->AltBody.
  284. if (strpos($body_part, 'text/plain')) {
  285. // Clean up the text.
  286. $body_part = trim($this->_remove_headers(trim($body_part)));
  287. // Include it as part of the mail object.
  288. $mailer->AltBody = $body_part;
  289. }
  290. // If plain/html within the body part, add it to $mailer->Body.
  291. elseif (strpos($body_part, 'text/html')) {
  292. // Clean up the text.
  293. $body_part = trim($this->_remove_headers(trim($body_part)));
  294. // Include it as part of the mail object.
  295. $mailer->Body = $body_part;
  296. }
  297. }
  298. break;
  299. case 'multipart/mixed':
  300. // Split the body based on the boundary ID.
  301. $body_parts = $this->_boundary_split($body, $boundary);
  302. // Determine if there is an HTML part for when adding the plain text part.
  303. $text_plain = FALSE;
  304. $text_html = FALSE;
  305. foreach ($body_parts as $body_part) {
  306. if (strpos($body_part, 'text/plain')) {
  307. $text_plain = TRUE;
  308. }
  309. if (strpos($body_part, 'text/html')) {
  310. $text_html = TRUE;
  311. }
  312. }
  313. foreach ($body_parts as $body_part) {
  314. // If test/plain within the body part, add it to either
  315. // $mailer->AltBody or $mailer->Body, depending on whether there is
  316. // also a text/html part ot not.
  317. if (strpos($body_part, 'multipart/alternative')) {
  318. // Get boundary ID from the Content-Type header.
  319. $boundary2 = $this->_get_substring($body_part, 'boundary', '"', '"');
  320. // Clean up the text.
  321. $body_part = trim($this->_remove_headers(trim($body_part)));
  322. // Split the body based on the boundary ID.
  323. $body_parts2 = $this->_boundary_split($body_part, $boundary2);
  324. foreach ($body_parts2 as $body_part2) {
  325. // If plain/text within the body part, add it to $mailer->AltBody.
  326. if (strpos($body_part2, 'text/plain')) {
  327. // Clean up the text.
  328. $body_part2 = trim($this->_remove_headers(trim($body_part2)));
  329. // Include it as part of the mail object.
  330. $mailer->AltBody = $body_part2;
  331. $mailer->ContentType = 'multipart/mixed';
  332. }
  333. // If plain/html within the body part, add it to $mailer->Body.
  334. elseif (strpos($body_part2, 'text/html')) {
  335. // Get the encoding.
  336. $body_part2_encoding = $this->_get_substring($body_part2, 'Content-Transfer-Encoding', ' ', "\n");
  337. // Clean up the text.
  338. $body_part2 = trim($this->_remove_headers(trim($body_part2)));
  339. // Check whether the encoding is base64, and if so, decode it.
  340. if (drupal_strtolower($body_part2_encoding) == 'base64') {
  341. // Include it as part of the mail object.
  342. $mailer->Body = base64_decode($body_part2);
  343. // Ensure the whole message is recoded in the base64 format.
  344. $mailer->Encoding = 'base64';
  345. }
  346. else {
  347. // Include it as part of the mail object.
  348. $mailer->Body = $body_part2;
  349. }
  350. $mailer->ContentType = 'multipart/mixed';
  351. }
  352. }
  353. }
  354. // If text/plain within the body part, add it to $mailer->Body.
  355. elseif (strpos($body_part, 'text/plain')) {
  356. // Clean up the text.
  357. $body_part = trim($this->_remove_headers(trim($body_part)));
  358. if ($text_html) {
  359. $mailer->AltBody = $body_part;
  360. $mailer->IsHTML(TRUE);
  361. $mailer->ContentType = 'multipart/mixed';
  362. }
  363. else {
  364. $mailer->Body = $body_part;
  365. $mailer->IsHTML(FALSE);
  366. $mailer->ContentType = 'multipart/mixed';
  367. }
  368. }
  369. // If text/html within the body part, add it to $mailer->Body.
  370. elseif (strpos($body_part, 'text/html')) {
  371. // Clean up the text.
  372. $body_part = trim($this->_remove_headers(trim($body_part)));
  373. // Include it as part of the mail object.
  374. $mailer->Body = $body_part;
  375. $mailer->IsHTML(TRUE);
  376. $mailer->ContentType = 'multipart/mixed';
  377. }
  378. // Add the attachment.
  379. elseif (strpos($body_part, 'Content-Disposition: attachment;')) {
  380. $file_path = $this->_get_substring($body_part, 'filename=', '"', '"');
  381. $file_name = $this->_get_substring($body_part, ' name=', '"', '"');
  382. $file_encoding = $this->_get_substring($body_part, 'Content-Transfer-Encoding', ' ', "\n");
  383. $file_type = $this->_get_substring($body_part, 'Content-Type', ' ', ';');
  384. if (file_exists($file_path)) {
  385. if (!$mailer->AddAttachment($file_path, $file_name, $file_encoding, $file_type)) {
  386. drupal_set_message(t('Attahment could not be found or accessed.'));
  387. }
  388. }
  389. else {
  390. // Clean up the text.
  391. $body_part = trim($this->_remove_headers(trim($body_part)));
  392. if (drupal_strtolower($file_encoding) == 'base64') {
  393. $attachment = base64_decode($body_part);
  394. }
  395. elseif (drupal_strtolower($file_encoding) == 'quoted-printable') {
  396. $attachment = quoted_printable_decode($body_part);
  397. }
  398. else {
  399. $attachment = $body_part;
  400. }
  401. $attachment_new_filename = drupal_tempnam('temporary://', 'smtp');
  402. $file_path = file_save_data($attachment, $attachment_new_filename, FILE_EXISTS_REPLACE);
  403. $real_path = drupal_realpath($file_path->uri);
  404. if (!$mailer->AddAttachment($real_path, $file_name)) {
  405. drupal_set_message(t('Attachment could not be found or accessed.'));
  406. }
  407. }
  408. }
  409. }
  410. break;
  411. default:
  412. $mailer->Body = $body;
  413. break;
  414. }
  415. // Process mimemail attachments
  416. if (isset($message['params']['attachments'])) {
  417. foreach ($message['params']['attachments'] as $attachment) {
  418. if (isset($attachment['filecontent'])) {
  419. $mailer->AddStringAttachment($attachment['filecontent'], $attachment['filename'], 'base64', $attachment['filemime']);
  420. }
  421. if (isset($attachment['filepath'])) {
  422. $mailer->AddAttachment($attachment['filepath'], $attachment['filename'], 'base64', $attachment['filemime']);
  423. }
  424. }
  425. }
  426. // Set the authentication settings.
  427. $username = variable_get('smtp_username', '');
  428. $password = variable_get('smtp_password', '');
  429. // If username and password are given, use SMTP authentication.
  430. if ($username != '' && $password != '') {
  431. $mailer->SMTPAuth = TRUE;
  432. $mailer->Username = $username;
  433. $mailer->Password = $password;
  434. }
  435. // Set the protocol prefix for the smtp host.
  436. switch (variable_get('smtp_protocol', 'standard')) {
  437. case 'ssl':
  438. $mailer->SMTPSecure = 'ssl';
  439. break;
  440. case 'tls':
  441. $mailer->SMTPSecure = 'tls';
  442. break;
  443. default:
  444. $mailer->SMTPSecure = '';
  445. }
  446. // Set other connection settings.
  447. $mailer->Host = variable_get('smtp_host', '') . ';' . variable_get('smtp_hostbackup', '');
  448. $mailer->Port = variable_get('smtp_port', '25');
  449. $mailer->Mailer = 'smtp';
  450. // Let the people know what is going on.
  451. watchdog('smtp', 'Sending mail to: @to', array('@to' => $to));
  452. // Try to send e-mail. If it fails, set watchdog entry.
  453. if (!$mailer->Send()) {
  454. watchdog('smtp', 'Error sending e-mail from @from to @to : !error_message', array('@from' => $from, '@to' => $to, '!error_message' => $mailer->ErrorInfo), WATCHDOG_ERROR);
  455. return FALSE;
  456. }
  457. $mailer->SmtpClose();
  458. return TRUE;
  459. }
  460. /**
  461. * Splits the input into parts based on the given boundary.
  462. *
  463. * Swiped from Mail::MimeDecode, with modifications based on Drupal's coding
  464. * standards and this bug report: http://pear.php.net/bugs/bug.php?id=6495
  465. *
  466. * @param input
  467. * A string containing the body text to parse.
  468. * @param boundary
  469. * A string with the boundary string to parse on.
  470. * @return
  471. * An array containing the resulting mime parts
  472. */
  473. protected function _boundary_split($input, $boundary) {
  474. $parts = array();
  475. $bs_possible = drupal_substr($boundary, 2, -2);
  476. $bs_check = '\"' . $bs_possible . '\"';
  477. if ($boundary == $bs_check) {
  478. $boundary = $bs_possible;
  479. }
  480. $tmp = explode('--' . $boundary, $input);
  481. for ($i = 1; $i < count($tmp); $i++) {
  482. if (trim($tmp[$i])) {
  483. $parts[] = $tmp[$i];
  484. }
  485. }
  486. return $parts;
  487. } // End of _smtp_boundary_split().
  488. /**
  489. * Strips the headers from the body part.
  490. *
  491. * @param input
  492. * A string containing the body part to strip.
  493. * @return
  494. * A string with the stripped body part.
  495. */
  496. protected function _remove_headers($input) {
  497. $part_array = explode("\n", $input);
  498. // will strip these headers according to RFC2045
  499. $headers_to_strip = array( 'Content-Type', 'Content-Transfer-Encoding', 'Content-ID', 'Content-Disposition');
  500. $pattern = '/^(' . implode('|', $headers_to_strip) . '):/';
  501. while (count($part_array) > 0) {
  502. // ignore trailing spaces/newlines
  503. $line = rtrim($part_array[0]);
  504. // if the line starts with a known header string
  505. if (preg_match($pattern, $line)) {
  506. $line = rtrim(array_shift($part_array));
  507. // remove line containing matched header.
  508. // if line ends in a ';' and the next line starts with four spaces, it's a continuation
  509. // of the header split onto the next line. Continue removing lines while we have this condition.
  510. while (substr($line, -1) == ';' && count($part_array) > 0 && substr($part_array[0], 0, 4) == ' ') {
  511. $line = rtrim(array_shift($part_array));
  512. }
  513. }
  514. else {
  515. // no match header, must be past headers; stop searching.
  516. break;
  517. }
  518. }
  519. $output = implode("\n", $part_array);
  520. return $output;
  521. } // End of _smtp_remove_headers().
  522. /**
  523. * Returns a string that is contained within another string.
  524. *
  525. * Returns the string from within $source that is some where after $target
  526. * and is between $beginning_character and $ending_character.
  527. *
  528. * @param $source
  529. * A string containing the text to look through.
  530. * @param $target
  531. * A string containing the text in $source to start looking from.
  532. * @param $beginning_character
  533. * A string containing the character just before the sought after text.
  534. * @param $ending_character
  535. * A string containing the character just after the sought after text.
  536. * @return
  537. * A string with the text found between the $beginning_character and the
  538. * $ending_character.
  539. */
  540. protected function _get_substring($source, $target, $beginning_character, $ending_character) {
  541. $search_start = strpos($source, $target) + 1;
  542. $first_character = strpos($source, $beginning_character, $search_start) + 1;
  543. $second_character = strpos($source, $ending_character, $first_character) + 1;
  544. $substring = drupal_substr($source, $first_character, $second_character - $first_character);
  545. $string_length = drupal_strlen($substring) - 1;
  546. if ($substring[$string_length] == $ending_character) {
  547. $substring = drupal_substr($substring, 0, $string_length);
  548. }
  549. return $substring;
  550. } // End of _smtp_get_substring().
  551. }