酒店预订平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

464 lines
12 KiB

  1. <?php
  2. /***************************************************\
  3. *
  4. * Mailer (https://github.com/txthinking/Mailer)
  5. *
  6. * A lightweight PHP SMTP mail sender.
  7. * Implement RFC0821, RFC0822, RFC1869, RFC2045, RFC2821
  8. *
  9. * Support html body, don't worry that the receiver's
  10. * mail client can't support html, because Mailer will
  11. * send both text/plain and text/html body, so if the
  12. * mail client can't support html, it will display the
  13. * text/plain body.
  14. *
  15. * Create Date 2012-07-25.
  16. * Under the MIT license.
  17. *
  18. \***************************************************/
  19. namespace Tx\Mailer;
  20. use Psr\Log\LoggerInterface;
  21. use Tx\Mailer\Exceptions\CodeException;
  22. use Tx\Mailer\Exceptions\CryptoException;
  23. use Tx\Mailer\Exceptions\SMTPException;
  24. class SMTP
  25. {
  26. /**
  27. * smtp socket
  28. */
  29. protected $smtp;
  30. /**
  31. * smtp server
  32. */
  33. protected $host;
  34. /**
  35. * smtp server port
  36. */
  37. protected $port;
  38. /**
  39. * smtp secure ssl tls tlsv1.0 tlsv1.1 tlsv1.2
  40. */
  41. protected $secure;
  42. /**
  43. * EHLO message
  44. */
  45. protected $ehlo;
  46. /**
  47. * smtp username
  48. */
  49. protected $username;
  50. /**
  51. * smtp password
  52. */
  53. protected $password;
  54. /**
  55. * oauth access token
  56. */
  57. protected $oauthToken;
  58. /**
  59. * $this->CRLF
  60. * @var string
  61. */
  62. protected $CRLF = "\r\n";
  63. /**
  64. * @var Message
  65. */
  66. protected $message;
  67. /**
  68. * @var LoggerInterface - Used to make things prettier than self::$logger
  69. */
  70. protected $logger;
  71. /**
  72. * Stack of all commands issued to SMTP
  73. * @var array
  74. */
  75. protected $commandStack = array();
  76. /**
  77. * Stack of all results issued to SMTP
  78. * @var array
  79. */
  80. protected $resultStack = array();
  81. public function __construct(LoggerInterface $logger=null)
  82. {
  83. $this->logger = $logger;
  84. }
  85. /**
  86. * set server and port
  87. * @param string $host server
  88. * @param int $port port
  89. * @param string $secure ssl tls tlsv1.0 tlsv1.1 tlsv1.2
  90. * @return $this
  91. */
  92. public function setServer($host, $port, $secure=null)
  93. {
  94. $this->host = $host;
  95. $this->port = $port;
  96. $this->secure = $secure;
  97. if(!$this->ehlo) $this->ehlo = $host;
  98. $this->logger && $this->logger->debug("Set: the server");
  99. return $this;
  100. }
  101. /**
  102. * auth login with server
  103. * @param string $username
  104. * @param string $password
  105. * @return $this
  106. */
  107. public function setAuth($username, $password)
  108. {
  109. $this->username = $username;
  110. $this->password = $password;
  111. $this->logger && $this->logger->debug("Set: the auth login");
  112. return $this;
  113. }
  114. /**
  115. * auth oauthbearer with server
  116. * @param string $accessToken
  117. * @return $this
  118. */
  119. public function setOAuth($accessToken)
  120. {
  121. $this->oauthToken = $accessToken;
  122. $this->logger && $this->logger->debug("Set: the auth oauthbearer");
  123. return $this;
  124. }
  125. /**
  126. * set the EHLO message
  127. * @param $ehlo
  128. * @return $this
  129. */
  130. public function setEhlo($ehlo)
  131. {
  132. $this->ehlo = $ehlo;
  133. return $this;
  134. }
  135. /**
  136. * Send the message
  137. *
  138. * @param Message $message
  139. * @return bool
  140. * @throws CodeException
  141. * @throws CryptoException
  142. * @throws SMTPException
  143. */
  144. public function send(Message $message)
  145. {
  146. $this->logger && $this->logger->debug('Set: a message will be sent');
  147. $this->message = $message;
  148. $this->connect()
  149. ->ehlo();
  150. if ($this->secure === 'tls' || $this->secure === 'tlsv1.0' || $this->secure === 'tlsv1.1' | $this->secure === 'tlsv1.2') {
  151. $this->starttls()
  152. ->ehlo();
  153. }
  154. if ($this->username !== null || $this->password !== null) {
  155. $this->authLogin();
  156. } elseif ($this->oauthToken !== null) {
  157. $this->authOAuthBearer();
  158. }
  159. $this->mailFrom()
  160. ->rcptTo()
  161. ->data()
  162. ->quit();
  163. return fclose($this->smtp);
  164. }
  165. /**
  166. * connect the server
  167. * SUCCESS 220
  168. * @return $this
  169. * @throws CodeException
  170. * @throws SMTPException
  171. */
  172. protected function connect()
  173. {
  174. $this->logger && $this->logger->debug("Connecting to {$this->host} at {$this->port}");
  175. $host = ($this->secure == 'ssl') ? 'ssl://' . $this->host : $this->host;
  176. $this->smtp = @fsockopen($host, $this->port);
  177. //set block mode
  178. // stream_set_blocking($this->smtp, 1);
  179. if (!$this->smtp){
  180. throw new SMTPException("Could not open SMTP Port.");
  181. }
  182. $code = $this->getCode();
  183. if ($code !== '220'){
  184. throw new CodeException('220', $code, array_pop($this->resultStack));
  185. }
  186. return $this;
  187. }
  188. /**
  189. * SMTP STARTTLS
  190. * SUCCESS 220
  191. * @return $this
  192. * @throws CodeException
  193. * @throws CryptoException
  194. * @throws SMTPException
  195. */
  196. protected function starttls()
  197. {
  198. $in = "STARTTLS" . $this->CRLF;
  199. $code = $this->pushStack($in);
  200. if ($code !== '220'){
  201. throw new CodeException('220', $code, array_pop($this->resultStack));
  202. }
  203. if ($this->secure !== 'tls' && version_compare(phpversion(), '5.6.0', '<')) {
  204. throw new CryptoException('Crypto type expected PHP 5.6 or greater');
  205. }
  206. switch ($this->secure) {
  207. case 'tlsv1.0':
  208. $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
  209. break;
  210. case 'tlsv1.1':
  211. $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
  212. break;
  213. case 'tlsv1.2':
  214. $crypto_type = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
  215. break;
  216. default:
  217. $crypto_type = STREAM_CRYPTO_METHOD_TLS_CLIENT;
  218. break;
  219. }
  220. if(!\stream_socket_enable_crypto($this->smtp, true, $crypto_type)) {
  221. throw new CryptoException("Start TLS failed to enable crypto");
  222. }
  223. return $this;
  224. }
  225. /**
  226. * SMTP EHLO
  227. * SUCCESS 250
  228. * @return $this
  229. * @throws CodeException
  230. * @throws SMTPException
  231. */
  232. protected function ehlo()
  233. {
  234. $in = "EHLO " . $this->ehlo . $this->CRLF;
  235. $code = $this->pushStack($in);
  236. if ($code !== '250'){
  237. throw new CodeException('250', $code, array_pop($this->resultStack));
  238. }
  239. return $this;
  240. }
  241. /**
  242. * SMTP AUTH LOGIN
  243. * SUCCESS 334
  244. * SUCCESS 334
  245. * SUCCESS 235
  246. * @return $this
  247. * @throws CodeException
  248. * @throws SMTPException
  249. */
  250. protected function authLogin()
  251. {
  252. $in = "AUTH LOGIN" . $this->CRLF;
  253. $code = $this->pushStack($in);
  254. if ($code !== '334'){
  255. throw new CodeException('334', $code, array_pop($this->resultStack));
  256. }
  257. $in = base64_encode($this->username) . $this->CRLF;
  258. $code = $this->pushStack($in);
  259. if ($code !== '334'){
  260. throw new CodeException('334', $code, array_pop($this->resultStack));
  261. }
  262. $in = base64_encode($this->password) . $this->CRLF;
  263. $code = $this->pushStack($in);
  264. if ($code !== '235'){
  265. throw new CodeException('235', $code, array_pop($this->resultStack));
  266. }
  267. return $this;
  268. }
  269. /**
  270. * SMTP AUTH OAUTHBEARER
  271. * SUCCESS 235
  272. * @return $this
  273. * @throws CodeException
  274. * @throws SMTPException
  275. */
  276. protected function authOAuthBearer()
  277. {
  278. $authStr = sprintf("n,a=%s,%shost=%s%sport=%s%sauth=Bearer %s%s%s",
  279. $this->message->getFromEmail(),
  280. chr(1),
  281. $this->host,
  282. chr(1),
  283. $this->port,
  284. chr(1),
  285. $this->oauthToken,
  286. chr(1),
  287. chr(1)
  288. );
  289. $authStr = base64_encode($authStr);
  290. $in = "AUTH OAUTHBEARER $authStr" . $this->CRLF;
  291. $code = $this->pushStack($in);
  292. if ($code !== '235'){
  293. throw new CodeException('235', $code, array_pop($this->resultStack));
  294. }
  295. return $this;
  296. }
  297. /**
  298. * SMTP AUTH XOAUTH2
  299. * SUCCESS 235
  300. * @return $this
  301. * @throws CodeException
  302. * @throws SMTPException
  303. */
  304. protected function authXOAuth2()
  305. {
  306. $authStr = sprintf("user=%s%sauth=Bearer %s%s%s",
  307. $this->message->getFromEmail(),
  308. chr(1),
  309. $this->oauthToken,
  310. chr(1),
  311. chr(1)
  312. );
  313. $authStr = base64_encode($authStr);
  314. $in = "AUTH XOAUTH2 $authStr" . $this->CRLF;
  315. $code = $this->pushStack($in);
  316. if ($code !== '235'){
  317. throw new CodeException('235', $code, array_pop($this->resultStack));
  318. }
  319. return $this;
  320. }
  321. /**
  322. * SMTP MAIL FROM
  323. * SUCCESS 250
  324. * @return $this
  325. * @throws CodeException
  326. * @throws SMTPException
  327. */
  328. protected function mailFrom()
  329. {
  330. $in = "MAIL FROM:<{$this->message->getFromEmail()}>" . $this->CRLF;
  331. $code = $this->pushStack($in);
  332. if ($code !== '250') {
  333. throw new CodeException('250', $code, array_pop($this->resultStack));
  334. }
  335. return $this;
  336. }
  337. /**
  338. * SMTP RCPT TO
  339. * SUCCESS 250
  340. * @return $this
  341. * @throws CodeException
  342. * @throws SMTPException
  343. */
  344. protected function rcptTo()
  345. {
  346. $to = array_merge(
  347. $this->message->getTo(),
  348. $this->message->getCc(),
  349. $this->message->getBcc()
  350. );
  351. foreach ($to as $toEmail=>$_) {
  352. $in = "RCPT TO:<" . $toEmail . ">" . $this->CRLF;
  353. $code = $this->pushStack($in);
  354. if ($code !== '250') {
  355. throw new CodeException('250', $code, array_pop($this->resultStack));
  356. }
  357. }
  358. return $this;
  359. }
  360. /**
  361. * SMTP DATA
  362. * SUCCESS 354
  363. * SUCCESS 250
  364. * @return $this
  365. * @throws CodeException
  366. * @throws SMTPException
  367. */
  368. protected function data()
  369. {
  370. $in = "DATA" . $this->CRLF;
  371. $code = $this->pushStack($in);
  372. if ($code !== '354') {
  373. throw new CodeException('354', $code, array_pop($this->resultStack));
  374. }
  375. $in = $this->message->toString();
  376. $code = $this->pushStack($in);
  377. if ($code !== '250'){
  378. throw new CodeException('250', $code, array_pop($this->resultStack));
  379. }
  380. return $this;
  381. }
  382. /**
  383. * SMTP QUIT
  384. * SUCCESS 221
  385. * @return $this
  386. * @throws CodeException
  387. * @throws SMTPException
  388. */
  389. protected function quit()
  390. {
  391. $in = "QUIT" . $this->CRLF;
  392. $code = $this->pushStack($in);
  393. if ($code !== '221'){
  394. throw new CodeException('221', $code, array_pop($this->resultStack));
  395. }
  396. return $this;
  397. }
  398. protected function pushStack($string)
  399. {
  400. $this->commandStack[] = $string;
  401. fputs($this->smtp, $string, strlen($string));
  402. $this->logger && $this->logger->debug('Sent: '. $string);
  403. return $this->getCode();
  404. }
  405. /**
  406. * get smtp response code
  407. * once time has three digital and a space
  408. * @return string
  409. * @throws SMTPException
  410. */
  411. protected function getCode()
  412. {
  413. while ($str = fgets($this->smtp, 515)) {
  414. $this->logger && $this->logger->debug("Got: ". $str);
  415. $this->resultStack[] = $str;
  416. if(substr($str,3,1) == " ") {
  417. $code = substr($str,0,3);
  418. return $code;
  419. }
  420. }
  421. throw new SMTPException("SMTP Server did not respond with anything I recognized");
  422. }
  423. }