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