1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Mail\Transport; 11 12use Traversable; 13use Zend\Mail; 14use Zend\Mail\Address\AddressInterface; 15use Zend\Mail\Exception; 16use Zend\Mail\Header\HeaderInterface; 17use Zend\Mail\Transport\Exception\RuntimeException; 18 19/** 20 * Class for sending email via the PHP internal mail() function 21 */ 22class Sendmail implements TransportInterface 23{ 24 /** 25 * Config options for sendmail parameters 26 * 27 * @var string 28 */ 29 protected $parameters; 30 31 /** 32 * Callback to use when sending mail; typically, {@link mailHandler()} 33 * 34 * @var callable 35 */ 36 protected $callable; 37 38 /** 39 * error information 40 * @var string 41 */ 42 protected $errstr; 43 44 /** 45 * @var string 46 */ 47 protected $operatingSystem; 48 49 /** 50 * Constructor. 51 * 52 * @param null|string|array|Traversable $parameters OPTIONAL (Default: null) 53 */ 54 public function __construct($parameters = null) 55 { 56 if ($parameters !== null) { 57 $this->setParameters($parameters); 58 } 59 $this->callable = array($this, 'mailHandler'); 60 } 61 62 /** 63 * Set sendmail parameters 64 * 65 * Used to populate the additional_parameters argument to mail() 66 * 67 * @param null|string|array|Traversable $parameters 68 * @throws \Zend\Mail\Exception\InvalidArgumentException 69 * @return Sendmail 70 */ 71 public function setParameters($parameters) 72 { 73 if ($parameters === null || is_string($parameters)) { 74 $this->parameters = $parameters; 75 return $this; 76 } 77 78 if (!is_array($parameters) && !$parameters instanceof Traversable) { 79 throw new Exception\InvalidArgumentException(sprintf( 80 '%s expects a string, array, or Traversable object of parameters; received "%s"', 81 __METHOD__, 82 (is_object($parameters) ? get_class($parameters) : gettype($parameters)) 83 )); 84 } 85 86 $string = ''; 87 foreach ($parameters as $param) { 88 $string .= ' ' . $param; 89 } 90 trim($string); 91 92 $this->parameters = $string; 93 return $this; 94 } 95 96 /** 97 * Set callback to use for mail 98 * 99 * Primarily for testing purposes, but could be used to curry arguments. 100 * 101 * @param callable $callable 102 * @throws \Zend\Mail\Exception\InvalidArgumentException 103 * @return Sendmail 104 */ 105 public function setCallable($callable) 106 { 107 if (!is_callable($callable)) { 108 throw new Exception\InvalidArgumentException(sprintf( 109 '%s expects a callable argument; received "%s"', 110 __METHOD__, 111 (is_object($callable) ? get_class($callable) : gettype($callable)) 112 )); 113 } 114 $this->callable = $callable; 115 return $this; 116 } 117 118 /** 119 * Send a message 120 * 121 * @param \Zend\Mail\Message $message 122 */ 123 public function send(Mail\Message $message) 124 { 125 $to = $this->prepareRecipients($message); 126 $subject = $this->prepareSubject($message); 127 $body = $this->prepareBody($message); 128 $headers = $this->prepareHeaders($message); 129 $params = $this->prepareParameters($message); 130 131 // On *nix platforms, we need to replace \r\n with \n 132 // sendmail is not an SMTP server, it is a unix command - it expects LF 133 if (!$this->isWindowsOs()) { 134 $to = str_replace("\r\n", "\n", $to); 135 $subject = str_replace("\r\n", "\n", $subject); 136 $body = str_replace("\r\n", "\n", $body); 137 $headers = str_replace("\r\n", "\n", $headers); 138 } 139 140 call_user_func($this->callable, $to, $subject, $body, $headers, $params); 141 } 142 143 /** 144 * Prepare recipients list 145 * 146 * @param \Zend\Mail\Message $message 147 * @throws \Zend\Mail\Exception\RuntimeException 148 * @return string 149 */ 150 protected function prepareRecipients(Mail\Message $message) 151 { 152 $headers = $message->getHeaders(); 153 154 if (!$headers->has('to')) { 155 throw new Exception\RuntimeException('Invalid email; contains no "To" header'); 156 } 157 158 $to = $headers->get('to'); 159 $list = $to->getAddressList(); 160 if (0 == count($list)) { 161 throw new Exception\RuntimeException('Invalid "To" header; contains no addresses'); 162 } 163 164 // If not on Windows, return normal string 165 if (!$this->isWindowsOs()) { 166 return $to->getFieldValue(HeaderInterface::FORMAT_ENCODED); 167 } 168 169 // Otherwise, return list of emails 170 $addresses = array(); 171 foreach ($list as $address) { 172 $addresses[] = $address->getEmail(); 173 } 174 $addresses = implode(', ', $addresses); 175 return $addresses; 176 } 177 178 /** 179 * Prepare the subject line string 180 * 181 * @param \Zend\Mail\Message $message 182 * @return string 183 */ 184 protected function prepareSubject(Mail\Message $message) 185 { 186 $headers = $message->getHeaders(); 187 if (!$headers->has('subject')) { 188 return; 189 } 190 $header = $headers->get('subject'); 191 return $header->getFieldValue(HeaderInterface::FORMAT_ENCODED); 192 } 193 194 /** 195 * Prepare the body string 196 * 197 * @param \Zend\Mail\Message $message 198 * @return string 199 */ 200 protected function prepareBody(Mail\Message $message) 201 { 202 if (!$this->isWindowsOs()) { 203 // *nix platforms can simply return the body text 204 return $message->getBodyText(); 205 } 206 207 // On windows, lines beginning with a full stop need to be fixed 208 $text = $message->getBodyText(); 209 $text = str_replace("\n.", "\n..", $text); 210 return $text; 211 } 212 213 /** 214 * Prepare the textual representation of headers 215 * 216 * @param \Zend\Mail\Message $message 217 * @return string 218 */ 219 protected function prepareHeaders(Mail\Message $message) 220 { 221 // On Windows, simply return verbatim 222 if ($this->isWindowsOs()) { 223 return $message->getHeaders()->toString(); 224 } 225 226 // On *nix platforms, strip the "to" header 227 $headers = clone $message->getHeaders(); 228 $headers->removeHeader('To'); 229 $headers->removeHeader('Subject'); 230 231 // Sanitize the From header 232 $from = $headers->get('From'); 233 if ($from) { 234 foreach ($from->getAddressList() as $address) { 235 if (preg_match('/\\\"/', $address->getEmail())) { 236 throw new RuntimeException('Potential code injection in From header'); 237 } 238 } 239 } 240 return $headers->toString(); 241 } 242 243 /** 244 * Prepare additional_parameters argument 245 * 246 * Basically, overrides the MAIL FROM envelope with either the Sender or 247 * From address. 248 * 249 * @param \Zend\Mail\Message $message 250 * @return string 251 */ 252 protected function prepareParameters(Mail\Message $message) 253 { 254 if ($this->isWindowsOs()) { 255 return; 256 } 257 258 $parameters = (string) $this->parameters; 259 260 $sender = $message->getSender(); 261 if ($sender instanceof AddressInterface) { 262 $parameters .= ' -f' . $sender->getEmail(); 263 return $parameters; 264 } 265 266 $from = $message->getFrom(); 267 if (count($from)) { 268 $from->rewind(); 269 $sender = $from->current(); 270 $parameters .= ' -f' . $sender->getEmail(); 271 return $parameters; 272 } 273 274 return $parameters; 275 } 276 277 /** 278 * Send mail using PHP native mail() 279 * 280 * @param string $to 281 * @param string $subject 282 * @param string $message 283 * @param string $headers 284 * @param $parameters 285 * @throws \Zend\Mail\Exception\RuntimeException 286 */ 287 public function mailHandler($to, $subject, $message, $headers, $parameters) 288 { 289 set_error_handler(array($this, 'handleMailErrors')); 290 if ($parameters === null) { 291 $result = mail($to, $subject, $message, $headers); 292 } else { 293 $result = mail($to, $subject, $message, $headers, $parameters); 294 } 295 restore_error_handler(); 296 297 if ($this->errstr !== null || !$result) { 298 $errstr = $this->errstr; 299 if (empty($errstr)) { 300 $errstr = 'Unknown error'; 301 } 302 throw new Exception\RuntimeException('Unable to send mail: ' . $errstr); 303 } 304 } 305 306 /** 307 * Temporary error handler for PHP native mail(). 308 * 309 * @param int $errno 310 * @param string $errstr 311 * @param string $errfile 312 * @param string $errline 313 * @param array $errcontext 314 * @return bool always true 315 */ 316 public function handleMailErrors($errno, $errstr, $errfile = null, $errline = null, array $errcontext = null) 317 { 318 $this->errstr = $errstr; 319 return true; 320 } 321 322 /** 323 * Is this a windows OS? 324 * 325 * @return bool 326 */ 327 protected function isWindowsOs() 328 { 329 if (!$this->operatingSystem) { 330 $this->operatingSystem = strtoupper(substr(PHP_OS, 0, 3)); 331 } 332 return ($this->operatingSystem == 'WIN'); 333 } 334} 335