PageRenderTime 54ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/sally/core/lib/sly/Mail.php

https://bitbucket.org/mediastuttgart/sallycms-0.6
PHP | 292 lines | 157 code | 48 blank | 87 comment | 13 complexity | 5b00c4bf29954e1cdead5b97ea71c1d1 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /*
  3. * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
  4. *
  5. * This file is released under the terms of the MIT license. You can find the
  6. * complete text in the attached LICENSE file or online at:
  7. *
  8. * http://www.opensource.org/licenses/mit-license.php
  9. */
  10. class sly_Mail implements sly_Mail_Interface {
  11. protected $tos; ///< array
  12. protected $from; ///< string
  13. protected $subject; ///< string
  14. protected $body; ///< string
  15. protected $contentType; ///< string
  16. protected $charset; ///< string
  17. protected $headers; ///< array
  18. /**
  19. * @throws sly_Mail_Exception when an extension returns a class that does not implement sly_Mail_Interface
  20. * @return sly_Mail_Interface
  21. */
  22. public static function factory() {
  23. $className = 'sly_Mail';
  24. $className = sly_Core::dispatcher()->filter('SLY_MAIL_CLASS', $className);
  25. $instance = new $className();
  26. if (!($instance instanceof sly_Mail_Interface)) {
  27. throw new sly_Mail_Exception(t('does_not_implement', $className, 'sly_Mail_Interface'));
  28. }
  29. return $instance;
  30. }
  31. public function __construct() {
  32. $this->tos = array();
  33. $this->from = '';
  34. $this->subject = '';
  35. $this->body = '';
  36. $this->contentType = 'text/plain';
  37. $this->charset = 'UTF-8';
  38. $this->headers = array();
  39. }
  40. /**
  41. * @param string $mail the address
  42. * @param string $name an optional name
  43. * @return sly_Mail_Interface self
  44. */
  45. public function addTo($mail, $name = null) {
  46. $this->tos[] = self::parseAddress($mail, $name);
  47. return $this;
  48. }
  49. /**
  50. * Clear recipients
  51. *
  52. * @return sly_Mail_Interface self
  53. */
  54. public function clearTo() {
  55. $this->tos = array();
  56. return $this;
  57. }
  58. /**
  59. * @param string $mail the address
  60. * @param string $name an optional name
  61. * @return sly_Mail_Interface self
  62. */
  63. public function setFrom($mail, $name = null) {
  64. $this->from = self::parseAddress($mail, $name);
  65. return $this;
  66. }
  67. /**
  68. * @param string $subject the new subject
  69. * @return sly_Mail_Interface self
  70. */
  71. public function setSubject($subject) {
  72. $this->subject = self::clean($subject);
  73. return $this;
  74. }
  75. /**
  76. * @param string $body the new body
  77. * @return sly_Mail_Interface self
  78. */
  79. public function setBody($body) {
  80. $this->body = self::clean($body);
  81. return $this;
  82. }
  83. /**
  84. * @param string $contentType the new content type
  85. * @return sly_Mail_Interface self
  86. */
  87. public function setContentType($contentType) {
  88. $this->contentType = strtolower(trim($contentType));
  89. return $this;
  90. }
  91. /**
  92. * @param string $charset the new charset
  93. * @return sly_Mail_Interface self
  94. */
  95. public function setCharset($charset) {
  96. $this->charset = strtoupper(trim($charset));
  97. return $this;
  98. }
  99. /**
  100. * @param string $field the header field (like 'x-foo')
  101. * @param string $value the header value (when empty, the corresponding header will be removed)
  102. * @return sly_Mail_Interface self
  103. */
  104. public function setHeader($field, $value) {
  105. $field = strtolower(trim($field));
  106. if ($value === false || $value === null || strlen(trim($value)) === 0) {
  107. unset($this->headers[$field]);
  108. }
  109. else {
  110. $this->headers[$field] = self::clean($value);
  111. }
  112. return $this;
  113. }
  114. /**
  115. * @throws sly_Mail_Exception when something is wrong
  116. * @return boolean always true
  117. */
  118. public function send() {
  119. $params = '';
  120. // do nothing if no one would read it
  121. if (empty($this->tos)) {
  122. throw new sly_Mail_Exception(t('no_recipients_given'));
  123. }
  124. // build recipient
  125. $to = array_map(array($this, 'buildAddress'), $this->tos);
  126. $to = implode(', ', array_unique($to));
  127. // build sender if available
  128. if (!empty($this->from)) {
  129. $this->setHeader('From', $this->buildAddress($this->from));
  130. $params = '-f'.$this->from[0]; // -fmy@sender.com
  131. }
  132. // encode subject
  133. $subject = $this->encode($this->subject);
  134. // set content type
  135. $this->setHeader('Content-Type', $this->contentType.'; charset='.$this->charset);
  136. // prepare headers
  137. $headers = array();
  138. foreach ($this->headers as $field => $value) {
  139. $headers[] = $field.': '.$value;
  140. }
  141. $headers = implode("\r\n", $headers);
  142. // and here we go
  143. if (!mail($to, $subject, $this->body, $headers, $params)) {
  144. throw new sly_Mail_Exception(t('error_sending_mail'));
  145. }
  146. return true;
  147. }
  148. /**
  149. * @param array $adress the address as an arra(mail, name)
  150. * @return string the final adress (only address or address with name)
  151. */
  152. protected function buildAddress($address) {
  153. list($mail, $name) = $address;
  154. if (strlen($name) === 0) {
  155. return $mail;
  156. }
  157. return $this->encode($name).' <'.$mail.'>';
  158. }
  159. /**
  160. * @throws sly_Mail_Exception when the address is invalid
  161. * @param string $mail the address
  162. * @param string $name the name (use null to give none)
  163. * @return array an array like array(mail, name)
  164. */
  165. protected static function parseAddress($mail, $name) {
  166. $mail = self::clean($mail);
  167. $name = $name === null ? null : self::clean($name);
  168. if (!self::isValid($mail)) {
  169. throw new sly_Mail_Exception(t('email_is_invalid', $mail));
  170. }
  171. return array($mail, $name);
  172. }
  173. /**
  174. * Cleans a string
  175. *
  176. * ASCII code characters excl. tab and CRLF. Matches any single non-printable
  177. * code character that may cause trouble in certain situations. Excludes tabs
  178. * and line breaks.
  179. *
  180. * @param string $str the string to clean
  181. * @return string the trimmed and cleaned string
  182. */
  183. protected static function clean($str) {
  184. return preg_replace('#[\x00\x08\x0B\x0C\x0E-\x1F]#', '', trim($str));
  185. }
  186. /**
  187. * @param string $str the string to encode
  188. * @return string the Base64 encoded string, marked with the current charset
  189. */
  190. public function encode($str) {
  191. if (!preg_match('#[^a-zA-Z0-9_+-]#', $str)) return $str;
  192. return '=?'.strtoupper($this->charset).'?B?'.base64_encode($str).'?=';
  193. }
  194. /**
  195. * @param string $address the address to validate
  196. * @return boolean true when valid according to the RFC, else false
  197. */
  198. public static function isValid($address) {
  199. $no_ws_ctl = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]";
  200. $alpha = "[\\x41-\\x5a\\x61-\\x7a]";
  201. $digit = "[\\x30-\\x39]";
  202. $cr = "\\x0d";
  203. $lf = "\\x0a";
  204. $crlf = "($cr$lf)";
  205. $obs_char = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]";
  206. $obs_text = "($lf*$cr*($obs_char$lf*$cr*)*)";
  207. $text = "([\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|$obs_text)";
  208. $obs_qp = "(\\x5c[\\x00-\\x7f])";
  209. $quoted_pair = "(\\x5c$text|$obs_qp)";
  210. $wsp = "[\\x20\\x09]";
  211. $obs_fws = "($wsp+($crlf$wsp+)*)";
  212. $fws = "((($wsp*$crlf)?$wsp+)|$obs_fws)";
  213. $ctext = "($no_ws_ctl|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])";
  214. $ccontent = "($ctext|$quoted_pair)";
  215. $comment = "(\\x28($fws?$ccontent)*$fws?\\x29)";
  216. $cfws = "(($fws?$comment)*($fws?$comment|$fws))";
  217. $cfws = "$fws*";
  218. $atext = "($alpha|$digit|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2f\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])";
  219. $atom = "($cfws?$atext+$cfws?)";
  220. $qtext = "($no_ws_ctl|[\\x21\\x23-\\x5b\\x5d-\\x7e])";
  221. $qcontent = "($qtext|$quoted_pair)";
  222. $quoted_string = "($cfws?\\x22($fws?$qcontent)*$fws?\\x22$cfws?)";
  223. $word = "($atom|$quoted_string)";
  224. $obs_local_part = "($word(\\x2e$word)*)";
  225. $obs_domain = "($atom(\\x2e$atom)*)";
  226. $dot_atom_text = "($atext+(\\x2e$atext+)*)";
  227. $dot_atom = "($cfws?$dot_atom_text$cfws?)";
  228. $dtext = "($no_ws_ctl|[\\x21-\\x5a\\x5e-\\x7e])";
  229. $dcontent = "($dtext|$quoted_pair)";
  230. $domain_literal = "($cfws?\\x5b($fws?$dcontent)*$fws?\\x5d$cfws?)";
  231. $local_part = "($dot_atom|$quoted_string|$obs_local_part)";
  232. $domain = "($dot_atom|$domain_literal|$obs_domain)";
  233. $addr_spec = "($local_part\\x40$domain)";
  234. $done = false;
  235. while (!$done) {
  236. $new = preg_replace("!$comment!", '', $address);
  237. if (strlen($new) === strlen($address)) {
  238. $done = true;
  239. }
  240. $address = $new;
  241. }
  242. return preg_match("!^$addr_spec$!", $address) ? true : false;
  243. }
  244. }