├── .gitattributes ├── .gitignore ├── examples └── 01-basic-usage.php ├── composer.json ├── LICENSE ├── README.md └── src └── Client.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | /tests/ 4 | 5 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 6 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 7 | composer.lock 8 | -------------------------------------------------------------------------------- /examples/01-basic-usage.php: -------------------------------------------------------------------------------- 1 | send('info@example.org', 'sergey.shuchkin@gmail.com', 'Test ReactPHP mailer', 'Hello, Sergey!')->then( 9 | function() { 10 | echo 'Message sent'.PHP_EOL; 11 | }, 12 | function ( \Exception $ex ) { 13 | echo 'SMTP error '.$ex->getCode().' '.$ex->getMessage().PHP_EOL; 14 | } 15 | ); 16 | 17 | $smtp->on('debug', function($s) { echo $s.PHP_EOL; }); 18 | 19 | $loop->run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shuchkin/react-smtp-client", 3 | "description": "ReactPHP async SMTP Client", 4 | "keywords": ["smtp", "reactphp", "mail","async","php" ], 5 | "homepage": "https://github.com/shuchkin/react-smtp-client", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Sergey Shuchkin (SMSPILOT)", 10 | "homepage": "https://github.com/shuchkin/" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3.0", 15 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 16 | "react/event-loop": "^1.0 || ^0.5", 17 | "react/socket": "^1.0 || ^0.8", 18 | "react/promise": "^2.2" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Shuchkin\\ReactSMTP\\": "src/" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sergey Shuchkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-smtp-client v0.2 2 | ====================== 3 | 4 | [ReactPHP](https://reactphp.org/) async SMTP client to send a emails like php 5 | mail(). Simple UTF-8 text/plain messages out-of-the-box. 6 | 7 | Basic Usage 8 | ----------- 9 | 10 | ~~~php 11 | $loop = \React\EventLoop\Factory::create(); 12 | 13 | $smtp = new \Shuchkin\ReactSMTP\Client( $loop ); // localhost:25 14 | 15 | $smtp->send('info@example.org', 'sergey.shuchkin@gmail.com', 'Test ReactPHP mailer', 'Hello, Sergey!')->then( 16 | function() { 17 | echo 'Message sent'.PHP_EOL; 18 | }, 19 | function ( \Exception $ex ) { 20 | echo 'SMTP error '.$ex->getCode().' '.$ex->getMessage().PHP_EOL; 21 | } 22 | ); 23 | 24 | $loop->run(); 25 | ~~~ 26 | 27 | Google SMTP Server – How to send bulk emails for free 28 | ----------------------------------------------------- 29 | 30 | ~~~php 31 | $loop = \React\EventLoop\Factory::create(); 32 | 33 | $smtp = new \Shuchkin\ReactSMTP\Client( $loop, 'tls://smtp.gmail.com:465', 'username@gmail.com','password' ); 34 | 35 | $recipients = ['sergey.shuchkin@gmail.com','example@example.com']; 36 | 37 | foreach( $recipients as $to ) { 38 | 39 | $smtp->send('username@gmail.com', $to, 'Test ReactPHP mailer', 'Hello, Sergey!')->then( 40 | function() use ( $to ) { 41 | echo 'Message to '.$to.' sent via Google SMTP'.PHP_EOL; 42 | }, 43 | function ( \Exception $ex ) use ( $to ) { 44 | echo 'Message to '.$to.' not sent: '.$ex->getMessage().PHP_EOL; 45 | } 46 | ); 47 | } 48 | 49 | $loop->run(); 50 | ~~~ 51 | 52 | Google limit for personal SMTP 99 messages per 24 hours. 53 | 54 | Using mime/mail class, send mails and attachments 55 | ------------------------------------------------- 56 | 57 | See https://github.com/shuchkin/simplemail 58 | 59 | ~~~bash 60 | $ composer require shuchkin/simplemail 61 | ~~~ 62 | 63 | ~~~php 64 | $smtp = new \Shuchkin\ReactSMTP\Client( $loop, 'example.com:25', 'username', 'password' ); 65 | 66 | // setup fabric 67 | $sm = new \Shuchkin\SimpleMail(); 68 | $sm->setFrom( 'example@example.com' ); 69 | $sm->setTransport( function ( \Shuchkin\SimpleMail $m, $encoded ) use ( $smtp ) { 70 | 71 | $smtp->send( $m->getFromEmail(), $encoded['to'], $encoded['subject'], $encoded['message'], $encoded['headers'] ) 72 | ->then( 73 | function () { 74 | echo "\r\nSent mail"; 75 | }, 76 | function ( \Exception $ex ) { 77 | echo "\r\n" . $ex->getMessage(); 78 | } 79 | ); 80 | }); 81 | 82 | // send mail 83 | $sm->to( ['sergey.shuchkin@gmail.com', 'reactphp@example.com'] ) 84 | ->setSubject('Async mail with ReactPHP') 85 | ->setText('Async mail sending perfect! See postcard') 86 | ->attach('image/postcard.jpg') 87 | ->send(); 88 | ~~~ 89 | 90 | Install 91 | ------- 92 | 93 | The recommended way to install this library is [through 94 | Composer](https://getcomposer.org). [New to 95 | Composer?](https://getcomposer.org/doc/00-intro.md) 96 | 97 | This will install the latest supported version: 98 | 99 | ~~~bash 100 | $ composer require shuchkin/react-smtp-client 101 | ~~~ 102 | 103 | Changelog 104 | --------- 105 | 106 | 0.2 (2020-02-19) - basic UTF-8 text/plain messages out-of-the-box, ReactPHP 107 | actual versions in composer.json 108 | 109 | 0.1.1 (2019-03-12) - Initial release -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 30 | $this->uri = $uri; 31 | $this->username = $username; 32 | $this->password = $password; 33 | $this->auth = ''; 34 | $this->queue = []; 35 | $this->lines = []; 36 | } 37 | 38 | public function send( $from, $to, $subject, $message, $headers = null ) { 39 | $deffered = new Deferred(); 40 | 41 | $from = strpos($from,'<') === false ? '<' . $from . '>' : $from; 42 | $lines = [ 'MAIL FROM: '. $from ]; 43 | 44 | if ( is_string( $to ) ) { 45 | $to = [ $to ]; 46 | } 47 | foreach( $to as $k => $t ) { 48 | $t = strpos($t,'<') === false ? '<'.$t.'>' : $t; 49 | $to[ $k ] = $t; 50 | $lines[] = 'RCPT TO: '.$t; 51 | } 52 | 53 | $headers_str = ''; 54 | 55 | if ( is_string( $headers )) { 56 | $headers_str = trim($headers)."\r\n"; 57 | } elseif ( is_array($headers)) { 58 | foreach ( $headers as $k => $v ) { 59 | $headers_str .= $k . ': ' . $v . "\r\n"; 60 | } 61 | } 62 | if ( stripos( $headers_str, 'From:') === false ) { 63 | $headers_str .= 'From: ' . $from . "\r\n"; 64 | } 65 | if ( stripos( $headers_str, 'To:') === false ) { 66 | $headers_str .= 'To: ' . implode( ', ', $to ) . "\r\n"; 67 | } 68 | if ( stripos( $headers_str, 'Subject:') === false ) { 69 | $headers_str .= 'Subject: =?UTF-8?B?'.base64_encode($subject)."?=\r\n"; 70 | } 71 | if ( stripos( $headers_str, 'Content-Type:') === false ) { 72 | $headers_str .= "Content-Type: text/plain; charset=UTF-8\r\n"; 73 | } 74 | 75 | 76 | $lines[] = 'DATA'; 77 | $lines[] = $headers_str . "\r\n" . $message . "\r\n."; 78 | 79 | $this->processQueue( $deffered, $lines ); 80 | 81 | return $deffered->promise(); 82 | } 83 | 84 | private function processQueue( $deffered = null, $lines = null ) { 85 | if ( $deffered ) { 86 | $this->queue[] = [ 'deffered' => $deffered, 'lines' => $lines ]; 87 | } 88 | if ( !$this->connector && !$this->conn ) { 89 | $this->connect(); 90 | return; 91 | } 92 | if ( count($this->lines) ) { 93 | return; 94 | } 95 | if ( $this->auth !== 'OK' ) { 96 | $this->lines[] = 'HELO server'; 97 | if ( ! empty( $this->username ) ) { 98 | $this->lines[] = 'AUTH LOGIN'; 99 | $this->lines[] = base64_encode( $this->username ); 100 | $this->lines[] = base64_encode( $this->password ); 101 | } 102 | $this->auth = 'LOGIN'; 103 | } elseif ( count( $this->queue ) ) { 104 | $this->buffer = ''; 105 | $m = array_shift( $this->queue ); 106 | $this->lines = $m['lines']; 107 | $this->deffered = $m['deffered']; 108 | if ( isset( $this->listeners['debug'] ) ) { 109 | $this->emit( 'debug', [ '----------- New message ----------' ] ); 110 | } 111 | // $this->handleData(''); 112 | } else { 113 | $this->close(); 114 | } 115 | } 116 | 117 | public function connect( $uri = null ) { 118 | if ( $uri ) { 119 | $this->uri = $uri; 120 | } 121 | $this->connector = new Connector( $this->loop ); 122 | 123 | /** @noinspection NullPointerExceptionInspection */ 124 | return $this->connector->connect( $this->uri )->then( 125 | function ( ConnectionInterface $conn ) { 126 | if ( isset( $this->listeners['debug'] ) ) { 127 | $this->emit( 'debug', [ 'Connected to ' . $this->uri ] ); 128 | } 129 | $this->buffer = ''; 130 | 131 | $conn->on( 'data', [ $this, 'handleData' ] ); 132 | $conn->on( 'end', [ $this, 'handleEnd' ] ); 133 | $conn->on( 'close', [ $this, 'handleClose' ] ); 134 | $conn->on( 'error', [ $this, 'handleError' ] ); 135 | $this->conn = $conn; 136 | $this->processQueue(); 137 | }, 138 | function ( Exception $ex ) { 139 | if ( isset( $this->listeners['debug'] ) ) { 140 | $this->emit( 'debug', [ $ex->getMessage() ] ); 141 | } 142 | foreach ( $this->queue as $m ) { 143 | $m['deffered']->reject( $ex, $m['deffered'] ); 144 | } 145 | $this->queue = []; 146 | } ); 147 | } 148 | 149 | public function handleData( $data ) { 150 | if ( isset( $this->listeners['debug'] ) ) { 151 | $this->emit( 'debug', [ 'S: ' . trim( $data ) ] ); 152 | } 153 | 154 | $this->buffer .= $data; 155 | if ( substr( $this->buffer, 3, 1 ) === '-' ) { 156 | return; 157 | } 158 | if ( strpos( $this->buffer, '5' ) === 0 ) { 159 | $this->close( new Exception( trim( $this->buffer) ) ); 160 | return; 161 | } 162 | 163 | if ( strpos( $this->buffer, '250' ) === 0 && ! count($this->lines) ) { 164 | $this->deffered->resolve( $this->buffer ); 165 | $this->reset(); 166 | $this->processQueue(); 167 | } 168 | 169 | if ( strpos( $this->buffer, '235' ) === 0 ) { 170 | $this->auth = 'OK'; 171 | $this->processQueue(); 172 | } 173 | $this->buffer = ''; 174 | if ( count( $this->lines ) ) { 175 | $line = array_shift( $this->lines ); 176 | $this->conn->write( $line . "\r\n" ); 177 | if ( isset( $this->listeners['debug'] ) ) { 178 | $this->emit( 'debug', [ 'C: ' . $line ] ); 179 | } 180 | } 181 | } 182 | 183 | /** 184 | * @param Exception|null $reason 185 | */ 186 | public function close( $reason = null ) { 187 | if ( !$reason ) { 188 | $reason = new Exception( 'Manually closed' ); 189 | } 190 | foreach ( $this->queue as $m ) { 191 | $m['deffered']->reject( $reason ); 192 | } 193 | $this->buffer = ''; 194 | $this->queue = []; 195 | $this->lines = []; 196 | $this->conn->close(); 197 | $this->conn = false; 198 | $this->connector = false; 199 | $this->auth = false; 200 | } 201 | 202 | private function reset() { 203 | $this->buffer = ''; 204 | $this->lines = []; 205 | $this->deffered = false; 206 | } 207 | 208 | public function handleEnd() { 209 | if ( isset( $this->listeners['debug'] ) ) { 210 | $this->emit( 'debug', [ 'Stream end ' . $this->uri ] ); 211 | } 212 | } 213 | 214 | public function handleClose() { 215 | if ( isset( $this->listeners['debug'] ) ) { 216 | $this->emit( 'debug', [ 'Disconnected from ' . $this->uri ] ); 217 | } 218 | foreach ( $this->queue as $m ) { 219 | $m['deffered']->reject( new Exception( 'Disconnected' ) ); 220 | } 221 | $this->queue = []; 222 | $this->reset(); 223 | } 224 | 225 | public function handleError( Exception $ex ) { 226 | if ( isset( $this->listeners['debug'] ) ) { 227 | $this->emit( 'debug', ['Error: ' . $ex->getMessage()] ); 228 | } 229 | /** @noinspection PhpUnhandledExceptionInspection */ 230 | throw $ex; 231 | } 232 | 233 | public function __destruct() { 234 | if ( $this->conn ) { 235 | $this->conn->end( "QUIT\r\n" ); 236 | } 237 | } 238 | } --------------------------------------------------------------------------------