PageRenderTime 84ms CodeModel.GetById 40ms app.highlight 12ms RepoModel.GetById 28ms app.codeStats 1ms

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

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