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