EsmtpTransport.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. <?php
  2. /*
  3. * This file is part of SwiftMailer.
  4. * (c) 2004-2009 Chris Corbyn
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * Sends Messages over SMTP with ESMTP support.
  11. *
  12. * @author Chris Corbyn
  13. */
  14. class Swift_Transport_EsmtpTransport extends Swift_Transport_AbstractSmtpTransport implements Swift_Transport_SmtpAgent
  15. {
  16. /**
  17. * ESMTP extension handlers.
  18. *
  19. * @var Swift_Transport_EsmtpHandler[]
  20. */
  21. private $handlers = [];
  22. /**
  23. * ESMTP capabilities.
  24. *
  25. * @var string[]
  26. */
  27. private $capabilities = [];
  28. /**
  29. * Connection buffer parameters.
  30. *
  31. * @var array
  32. */
  33. private $params = [
  34. 'protocol' => 'tcp',
  35. 'host' => 'localhost',
  36. 'port' => 25,
  37. 'timeout' => 30,
  38. 'blocking' => 1,
  39. 'tls' => false,
  40. 'type' => Swift_Transport_IoBuffer::TYPE_SOCKET,
  41. 'stream_context_options' => [],
  42. ];
  43. /**
  44. * Creates a new EsmtpTransport using the given I/O buffer.
  45. *
  46. * @param Swift_Transport_EsmtpHandler[] $extensionHandlers
  47. * @param string $localDomain
  48. */
  49. public function __construct(Swift_Transport_IoBuffer $buf, array $extensionHandlers, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1', Swift_AddressEncoder $addressEncoder = null)
  50. {
  51. parent::__construct($buf, $dispatcher, $localDomain, $addressEncoder);
  52. $this->setExtensionHandlers($extensionHandlers);
  53. }
  54. /**
  55. * Set the host to connect to.
  56. *
  57. * Literal IPv6 addresses should be wrapped in square brackets.
  58. *
  59. * @param string $host
  60. *
  61. * @return $this
  62. */
  63. public function setHost($host)
  64. {
  65. $this->params['host'] = $host;
  66. return $this;
  67. }
  68. /**
  69. * Get the host to connect to.
  70. *
  71. * @return string
  72. */
  73. public function getHost()
  74. {
  75. return $this->params['host'];
  76. }
  77. /**
  78. * Set the port to connect to.
  79. *
  80. * @param int $port
  81. *
  82. * @return $this
  83. */
  84. public function setPort($port)
  85. {
  86. $this->params['port'] = (int) $port;
  87. return $this;
  88. }
  89. /**
  90. * Get the port to connect to.
  91. *
  92. * @return int
  93. */
  94. public function getPort()
  95. {
  96. return $this->params['port'];
  97. }
  98. /**
  99. * Set the connection timeout.
  100. *
  101. * @param int $timeout seconds
  102. *
  103. * @return $this
  104. */
  105. public function setTimeout($timeout)
  106. {
  107. $this->params['timeout'] = (int) $timeout;
  108. $this->buffer->setParam('timeout', (int) $timeout);
  109. return $this;
  110. }
  111. /**
  112. * Get the connection timeout.
  113. *
  114. * @return int
  115. */
  116. public function getTimeout()
  117. {
  118. return $this->params['timeout'];
  119. }
  120. /**
  121. * Set the encryption type (tls or ssl).
  122. *
  123. * @param string $encryption
  124. *
  125. * @return $this
  126. */
  127. public function setEncryption($encryption)
  128. {
  129. $encryption = strtolower($encryption ?? '');
  130. if ('tls' == $encryption) {
  131. $this->params['protocol'] = 'tcp';
  132. $this->params['tls'] = true;
  133. } else {
  134. $this->params['protocol'] = $encryption;
  135. $this->params['tls'] = false;
  136. }
  137. return $this;
  138. }
  139. /**
  140. * Get the encryption type.
  141. *
  142. * @return string
  143. */
  144. public function getEncryption()
  145. {
  146. return $this->params['tls'] ? 'tls' : $this->params['protocol'];
  147. }
  148. /**
  149. * Sets the stream context options.
  150. *
  151. * @param array $options
  152. *
  153. * @return $this
  154. */
  155. public function setStreamOptions($options)
  156. {
  157. $this->params['stream_context_options'] = $options;
  158. return $this;
  159. }
  160. /**
  161. * Returns the stream context options.
  162. *
  163. * @return array
  164. */
  165. public function getStreamOptions()
  166. {
  167. return $this->params['stream_context_options'];
  168. }
  169. /**
  170. * Sets the source IP.
  171. *
  172. * IPv6 addresses should be wrapped in square brackets.
  173. *
  174. * @param string $source
  175. *
  176. * @return $this
  177. */
  178. public function setSourceIp($source)
  179. {
  180. $this->params['sourceIp'] = $source;
  181. return $this;
  182. }
  183. /**
  184. * Returns the IP used to connect to the destination.
  185. *
  186. * @return string
  187. */
  188. public function getSourceIp()
  189. {
  190. return $this->params['sourceIp'] ?? null;
  191. }
  192. /**
  193. * Sets whether SMTP pipelining is enabled.
  194. *
  195. * By default, support is auto-detected using the PIPELINING SMTP extension.
  196. * Use this function to override that in the unlikely event of compatibility
  197. * issues.
  198. *
  199. * @param bool $enabled
  200. *
  201. * @return $this
  202. */
  203. public function setPipelining($enabled)
  204. {
  205. $this->pipelining = $enabled;
  206. return $this;
  207. }
  208. /**
  209. * Returns whether SMTP pipelining is enabled.
  210. *
  211. * @return bool|null a boolean if pipelining is explicitly enabled or disabled,
  212. * or null if support is auto-detected
  213. */
  214. public function getPipelining()
  215. {
  216. return $this->pipelining;
  217. }
  218. /**
  219. * Set ESMTP extension handlers.
  220. *
  221. * @param Swift_Transport_EsmtpHandler[] $handlers
  222. *
  223. * @return $this
  224. */
  225. public function setExtensionHandlers(array $handlers)
  226. {
  227. $assoc = [];
  228. foreach ($handlers as $handler) {
  229. $assoc[$handler->getHandledKeyword()] = $handler;
  230. }
  231. uasort($assoc, function ($a, $b) {
  232. return $a->getPriorityOver($b->getHandledKeyword());
  233. });
  234. $this->handlers = $assoc;
  235. $this->setHandlerParams();
  236. return $this;
  237. }
  238. /**
  239. * Get ESMTP extension handlers.
  240. *
  241. * @return Swift_Transport_EsmtpHandler[]
  242. */
  243. public function getExtensionHandlers()
  244. {
  245. return array_values($this->handlers);
  246. }
  247. /**
  248. * Run a command against the buffer, expecting the given response codes.
  249. *
  250. * If no response codes are given, the response will not be validated.
  251. * If codes are given, an exception will be thrown on an invalid response.
  252. *
  253. * @param string $command
  254. * @param int[] $codes
  255. * @param string[] $failures An array of failures by-reference
  256. * @param bool $pipeline Do not wait for response
  257. * @param string $address the address, if command is RCPT TO
  258. *
  259. * @return string|null The server response, or null if pipelining is enabled
  260. */
  261. public function executeCommand($command, $codes = [], &$failures = null, $pipeline = false, $address = null)
  262. {
  263. $failures = (array) $failures;
  264. $stopSignal = false;
  265. $response = null;
  266. foreach ($this->getActiveHandlers() as $handler) {
  267. $response = $handler->onCommand(
  268. $this, $command, $codes, $failures, $stopSignal
  269. );
  270. if ($stopSignal) {
  271. return $response;
  272. }
  273. }
  274. return parent::executeCommand($command, $codes, $failures, $pipeline, $address);
  275. }
  276. /** Mixin handling method for ESMTP handlers */
  277. public function __call($method, $args)
  278. {
  279. foreach ($this->handlers as $handler) {
  280. if (\in_array(strtolower($method),
  281. array_map('strtolower', (array) $handler->exposeMixinMethods())
  282. )) {
  283. $return = \call_user_func_array([$handler, $method], $args);
  284. // Allow fluid method calls
  285. if (null === $return && 'set' == substr($method, 0, 3)) {
  286. return $this;
  287. } else {
  288. return $return;
  289. }
  290. }
  291. }
  292. trigger_error('Call to undefined method '.$method, E_USER_ERROR);
  293. }
  294. /** Get the params to initialize the buffer */
  295. protected function getBufferParams()
  296. {
  297. return $this->params;
  298. }
  299. /** Overridden to perform EHLO instead */
  300. protected function doHeloCommand()
  301. {
  302. try {
  303. $response = $this->executeCommand(
  304. sprintf("EHLO %s\r\n", $this->domain), [250]
  305. );
  306. } catch (Swift_TransportException $e) {
  307. return parent::doHeloCommand();
  308. }
  309. if ($this->params['tls']) {
  310. try {
  311. $this->executeCommand("STARTTLS\r\n", [220]);
  312. if (!$this->buffer->startTLS()) {
  313. throw new Swift_TransportException('Unable to connect with TLS encryption');
  314. }
  315. try {
  316. $response = $this->executeCommand(
  317. sprintf("EHLO %s\r\n", $this->domain), [250]
  318. );
  319. } catch (Swift_TransportException $e) {
  320. return parent::doHeloCommand();
  321. }
  322. } catch (Swift_TransportException $e) {
  323. $this->throwException($e);
  324. }
  325. }
  326. $this->capabilities = $this->getCapabilities($response);
  327. if (!isset($this->pipelining)) {
  328. $this->pipelining = isset($this->capabilities['PIPELINING']);
  329. }
  330. $this->setHandlerParams();
  331. foreach ($this->getActiveHandlers() as $handler) {
  332. $handler->afterEhlo($this);
  333. }
  334. }
  335. /** Overridden to add Extension support */
  336. protected function doMailFromCommand($address)
  337. {
  338. $address = $this->addressEncoder->encodeString($address);
  339. $handlers = $this->getActiveHandlers();
  340. $params = [];
  341. foreach ($handlers as $handler) {
  342. $params = array_merge($params, (array) $handler->getMailParams());
  343. }
  344. $paramStr = !empty($params) ? ' '.implode(' ', $params) : '';
  345. $this->executeCommand(
  346. sprintf("MAIL FROM:<%s>%s\r\n", $address, $paramStr), [250], $failures, true
  347. );
  348. }
  349. /** Overridden to add Extension support */
  350. protected function doRcptToCommand($address)
  351. {
  352. $address = $this->addressEncoder->encodeString($address);
  353. $handlers = $this->getActiveHandlers();
  354. $params = [];
  355. foreach ($handlers as $handler) {
  356. $params = array_merge($params, (array) $handler->getRcptParams());
  357. }
  358. $paramStr = !empty($params) ? ' '.implode(' ', $params) : '';
  359. $this->executeCommand(
  360. sprintf("RCPT TO:<%s>%s\r\n", $address, $paramStr), [250, 251, 252], $failures, true, $address
  361. );
  362. }
  363. /** Determine ESMTP capabilities by function group */
  364. private function getCapabilities($ehloResponse)
  365. {
  366. $capabilities = [];
  367. $ehloResponse = trim($ehloResponse ?? '');
  368. $lines = explode("\r\n", $ehloResponse);
  369. array_shift($lines);
  370. foreach ($lines as $line) {
  371. if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
  372. $keyword = strtoupper($matches[1]);
  373. $paramStr = strtoupper(ltrim($matches[2], ' ='));
  374. $params = !empty($paramStr) ? explode(' ', $paramStr) : [];
  375. $capabilities[$keyword] = $params;
  376. }
  377. }
  378. return $capabilities;
  379. }
  380. /** Set parameters which are used by each extension handler */
  381. private function setHandlerParams()
  382. {
  383. foreach ($this->handlers as $keyword => $handler) {
  384. if (\array_key_exists($keyword, $this->capabilities)) {
  385. $handler->setKeywordParams($this->capabilities[$keyword]);
  386. }
  387. }
  388. }
  389. /** Get ESMTP handlers which are currently ok to use */
  390. private function getActiveHandlers()
  391. {
  392. $handlers = [];
  393. foreach ($this->handlers as $keyword => $handler) {
  394. if (\array_key_exists($keyword, $this->capabilities)) {
  395. $handlers[] = $handler;
  396. }
  397. }
  398. return $handlers;
  399. }
  400. }