+ * @author Nicolas Grekas
+ * @author Chris Corbyn
+ *
+ * @internal
+ *
+ * @experimental in 4.3
+ */
+abstract class AbstractStream
+{
+ protected $stream;
+ protected $in;
+ protected $out;
+
+ public function write(string $bytes): void
+ {
+ $bytesToWrite = \strlen($bytes);
+ $totalBytesWritten = 0;
+ while ($totalBytesWritten < $bytesToWrite) {
+ $bytesWritten = fwrite($this->in, substr($bytes, $totalBytesWritten));
+ if (false === $bytesWritten || 0 === $bytesWritten) {
+ throw new TransportException('Unable to write bytes on the wire.');
+ }
+
+ $totalBytesWritten += $bytesWritten;
+ }
+ }
+
+ /**
+ * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning.
+ */
+ public function flush(): void
+ {
+ fflush($this->in);
+ }
+
+ /**
+ * Performs any initialization needed.
+ */
+ abstract public function initialize(): void;
+
+ public function terminate(): void
+ {
+ $this->stream = $this->out = $this->in = null;
+ }
+
+ public function readLine(): string
+ {
+ if (feof($this->out)) {
+ return '';
+ }
+
+ $line = fgets($this->out);
+ if (0 === \strlen($line)) {
+ $metas = stream_get_meta_data($this->out);
+ if ($metas['timed_out']) {
+ throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription()));
+ }
+ }
+
+ return $line;
+ }
+
+ public static function replace(string $from, string $to, iterable $chunks): \Generator
+ {
+ if ('' === $from) {
+ yield from $chunks;
+
+ return;
+ }
+
+ $carry = '';
+ $fromLen = \strlen($from);
+
+ foreach ($chunks as $chunk) {
+ if ('' === $chunk = $carry.$chunk) {
+ continue;
+ }
+
+ if (false !== strpos($chunk, $from)) {
+ $chunk = explode($from, $chunk);
+ $carry = array_pop($chunk);
+
+ yield implode($to, $chunk).$to;
+ } else {
+ $carry = $chunk;
+ }
+
+ if (\strlen($carry) > $fromLen) {
+ yield substr($carry, 0, -$fromLen);
+ $carry = substr($carry, -$fromLen);
+ }
+ }
+
+ if ('' !== $carry) {
+ yield $carry;
+ }
+ }
+
+ abstract protected function getReadConnectionDescription(): string;
+}
diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php
new file mode 100644
index 000000000000..dfbf930840d8
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport\Smtp\Stream;
+
+use Symfony\Component\Mailer\Exception\TransportException;
+
+/**
+ * A stream supporting local processes.
+ *
+ * @author Fabien Potencier
+ * @author Chris Corbyn
+ *
+ * @internal
+ *
+ * @experimental in 4.3
+ */
+final class ProcessStream extends AbstractStream
+{
+ private $command;
+
+ public function setCommand(string $command)
+ {
+ $this->command = $command;
+ }
+
+ public function initialize(): void
+ {
+ $descriptorSpec = [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ];
+ $pipes = [];
+ $this->stream = proc_open($this->command, $descriptorSpec, $pipes);
+ stream_set_blocking($pipes[2], false);
+ if ($err = stream_get_contents($pipes[2])) {
+ throw new TransportException(sprintf('Process could not be started: %s.', $err));
+ }
+ $this->in = &$pipes[0];
+ $this->out = &$pipes[1];
+ }
+
+ public function terminate(): void
+ {
+ if (null !== $this->stream) {
+ fclose($this->in);
+ fclose($this->out);
+ proc_close($this->stream);
+ }
+
+ parent::terminate();
+ }
+
+ protected function getReadConnectionDescription(): string
+ {
+ return 'process '.$this->command;
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/SocketStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/SocketStream.php
new file mode 100644
index 000000000000..07692b11bac7
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/SocketStream.php
@@ -0,0 +1,172 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport\Smtp\Stream;
+
+use Symfony\Component\Mailer\Exception\TransportException;
+
+/**
+ * A stream supporting remote sockets.
+ *
+ * @author Fabien Potencier
+ * @author Chris Corbyn
+ *
+ * @internal
+ *
+ * @experimental in 4.3
+ */
+final class SocketStream extends AbstractStream
+{
+ private $url;
+ private $host = 'localhost';
+ private $protocol = 'tcp';
+ private $port = 25;
+ private $timeout = 15;
+ private $tls = false;
+ private $sourceIp;
+ private $streamContextOptions = [];
+
+ public function setTimeout(int $timeout): self
+ {
+ $this->timeout = $timeout;
+
+ return $this;
+ }
+
+ public function getTimeout(): int
+ {
+ return $this->timeout;
+ }
+
+ /**
+ * Literal IPv6 addresses should be wrapped in square brackets.
+ */
+ public function setHost(string $host): self
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ public function setPort(int $port): self
+ {
+ $this->port = $port;
+
+ return $this;
+ }
+
+ public function getPort(): int
+ {
+ return $this->port;
+ }
+
+ /**
+ * Sets the encryption type (tls or ssl).
+ */
+ public function setEncryption(string $encryption): self
+ {
+ $encryption = strtolower($encryption);
+ if ('tls' === $encryption) {
+ $this->protocol = 'tcp';
+ $this->tls = true;
+ } else {
+ $this->protocol = $encryption;
+ $this->tls = false;
+ }
+
+ return $this;
+ }
+
+ public function isTLS(): bool
+ {
+ return $this->tls;
+ }
+
+ public function setStreamOptions(array $options): self
+ {
+ $this->streamContextOptions = $options;
+
+ return $this;
+ }
+
+ public function getStreamOptions(): array
+ {
+ return $this->streamContextOptions;
+ }
+
+ /**
+ * Sets the source IP.
+ *
+ * IPv6 addresses should be wrapped in square brackets.
+ */
+ public function setSourceIp(string $ip): self
+ {
+ $this->sourceIp = $ip;
+
+ return $this;
+ }
+
+ /**
+ * Returns the IP used to connect to the destination.
+ */
+ public function getSourceIp(): ?string
+ {
+ return $this->sourceIp;
+ }
+
+ public function initialize(): void
+ {
+ $this->url = $this->host.':'.$this->port;
+ if ($this->protocol) {
+ $this->url = $this->protocol.'://'.$this->url;
+ }
+ $options = [];
+ if ($this->sourceIp) {
+ $options['socket']['bindto'] = $this->sourceIp.':0';
+ }
+ if ($this->streamContextOptions) {
+ $options = array_merge($options, $this->streamContextOptions);
+ }
+ $streamContext = stream_context_create($options);
+ $this->stream = @stream_socket_client($this->url, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $streamContext);
+ if (false === $this->stream) {
+ throw new TransportException(sprintf('Connection could not be established with host "%s": %s (%s)', $this->url, $errstr, $errno));
+ }
+ stream_set_blocking($this->stream, true);
+ stream_set_timeout($this->stream, $this->timeout);
+ $this->in = &$this->stream;
+ $this->out = &$this->stream;
+ }
+
+ public function startTLS(): bool
+ {
+ return (bool) stream_socket_enable_crypto($this->stream, true);
+ }
+
+ public function terminate(): void
+ {
+ if (null !== $this->stream) {
+ fclose($this->stream);
+ }
+
+ parent::terminate();
+ }
+
+ protected function getReadConnectionDescription(): string
+ {
+ return $this->url;
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/TransportInterface.php b/src/Symfony/Component/Mailer/Transport/TransportInterface.php
new file mode 100644
index 000000000000..852db42be78e
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/TransportInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\SmtpEnvelope;
+use Symfony\Component\Mime\RawMessage;
+
+/**
+ * Interface for all mailer transports.
+ *
+ * When sending emails, you should prefer MailerInterface implementations
+ * as they allow asynchronous sending.
+ *
+ * @author Fabien Potencier
+ *
+ * @experimental in 4.3
+ */
+interface TransportInterface
+{
+ /**
+ * @throws TransportExceptionInterface
+ */
+ public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage;
+}
diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json
new file mode 100644
index 000000000000..27a5db082a73
--- /dev/null
+++ b/src/Symfony/Component/Mailer/composer.json
@@ -0,0 +1,45 @@
+{
+ "name": "symfony/mailer",
+ "type": "library",
+ "description": "Symfony Mailer Component",
+ "keywords": [],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": "^7.1.3",
+ "psr/log": "~1.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/mime": "^4.3"
+ },
+ "require-dev": {
+ "symfony/amazon-mailer": "^4.3",
+ "egulias/email-validator": "^2.0",
+ "symfony/google-mailer": "^4.3",
+ "symfony/mailgun-mailer": "^4.3",
+ "symfony/mailchimp-mailer": "^4.3",
+ "symfony/postmark-mailer": "^4.3",
+ "symfony/sendgrid-mailer": "^4.3"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Mailer\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ }
+}
diff --git a/src/Symfony/Component/Mailer/phpunit.xml.dist b/src/Symfony/Component/Mailer/phpunit.xml.dist
new file mode 100644
index 000000000000..adcc4721d47a
--- /dev/null
+++ b/src/Symfony/Component/Mailer/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+ ./Tests
+ ./vendor
+
+
+
+