FakeSMTPHelper.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. // Code below modified from https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/fakeSMTP.php,
  7. // which is under the MIT license (https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/LICENSE)
  8. /**
  9. * fakeSMTP - A PHP / inetd fake smtp server.
  10. * Allows client<->server interaction
  11. * The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt
  12. */
  13. class fakeSMTP {
  14. public $logFile = false;
  15. public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready';
  16. public function __construct($fd) {
  17. $this->mail = [];
  18. $this->mail['ipaddress'] = false;
  19. $this->mail['emailSender'] = '';
  20. $this->mail['emailRecipients'] = [];
  21. $this->mail['emailSubject'] = false;
  22. $this->mail['rawEmail'] = false;
  23. $this->mail['emailHeaders'] = false;
  24. $this->mail['emailBody'] = false;
  25. $this->fd = $fd;
  26. }
  27. public function receive() {
  28. $hasValidFrom = false;
  29. $hasValidTo = false;
  30. $receivingData = false;
  31. $header = true;
  32. $this->reply('220 '.$this->serverHello);
  33. $this->mail['ipaddress'] = $this->detectIP();
  34. while ($data = fgets($this->fd)) {
  35. $data = preg_replace('@\r\n@', "\n", $data);
  36. if (!$receivingData) {
  37. $this->log($data);
  38. }
  39. if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) {
  40. if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) {
  41. $this->mail['emailSender'] = $match[1];
  42. $this->reply('250 2.1.0 Ok');
  43. $hasValidFrom = true;
  44. } else {
  45. $this->reply('551 5.1.7 Bad sender address syntax');
  46. }
  47. } elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) {
  48. if (!$hasValidFrom) {
  49. $this->reply('503 5.5.1 Error: need MAIL command');
  50. } else {
  51. if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) {
  52. array_push($this->mail['emailRecipients'], $match[1]);
  53. $this->reply('250 2.1.5 Ok');
  54. $hasValidTo = true;
  55. } else {
  56. $this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]);
  57. }
  58. }
  59. } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) {
  60. $this->reply('250 2.0.0 Ok');
  61. $hasValidFrom = false;
  62. $hasValidTo = false;
  63. } elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) {
  64. $this->reply('250 2.0.0 Ok');
  65. } elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) {
  66. $this->reply('250 2.0.0 '.$match[1]);
  67. } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) {
  68. if (!$hasValidTo) {
  69. $this->reply('503 5.5.1 Error: need RCPT command');
  70. } else {
  71. $this->reply('354 Ok Send data ending with <CRLF>.<CRLF>');
  72. $receivingData = true;
  73. }
  74. } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) {
  75. $this->reply('250 HELO '.$this->mail['ipaddress']);
  76. } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) {
  77. break;
  78. } elseif (!$receivingData) {
  79. //~ $this->reply('250 Ok');
  80. $this->reply('502 5.5.2 Error: command not recognized');
  81. } elseif ($receivingData && $data == ".\n") {
  82. /* Email Received, now let's look at it */
  83. $receivingData = false;
  84. $this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10));
  85. $splitmail = explode("\n\n", $this->mail['rawEmail'], 2);
  86. if (count($splitmail) == 2) {
  87. $this->mail['emailHeaders'] = $splitmail[0];
  88. $this->mail['emailBody'] = $splitmail[1];
  89. $headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders']));
  90. $headerlines = explode("\n", $headers);
  91. for ($i = 0; $i < count($headerlines); $i++) {
  92. if (preg_match('/^Subject: (.*)/i', $headerlines[$i], $matches)) {
  93. $this->mail['emailSubject'] = trim($matches[1]);
  94. }
  95. }
  96. } else {
  97. $this->mail['emailBody'] = $splitmail[0];
  98. }
  99. set_time_limit(5); // Just run the exit to prevent open threads / abuse
  100. } elseif ($receivingData) {
  101. $this->mail['rawEmail'] .= $data;
  102. }
  103. }
  104. /* Say good bye */
  105. $this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']);
  106. fclose($this->fd);
  107. }
  108. public function log($s) {
  109. if ($this->logFile) {
  110. file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND);
  111. }
  112. }
  113. private function reply($s) {
  114. $this->log("REPLY:$s");
  115. fwrite($this->fd, $s . "\r\n");
  116. }
  117. private function detectIP() {
  118. $raw = explode(':', stream_socket_get_name($this->fd, true));
  119. return $raw[0];
  120. }
  121. private function validateEmail($email) {
  122. return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email));
  123. }
  124. private function generateRandom($length = 8) {
  125. $password = '';
  126. $possible = '2346789BCDFGHJKLMNPQRTVWXYZ';
  127. $maxlength = strlen($possible);
  128. $i = 0;
  129. for ($i = 0; $i < $length; $i++) {
  130. $char = substr($possible, mt_rand(0, $maxlength - 1), 1);
  131. if (!strstr($password, $char)) {
  132. $password .= $char;
  133. }
  134. }
  135. return $password;
  136. }
  137. }
  138. $socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr);
  139. if (!$socket) {
  140. exit();
  141. }
  142. while ($fd = stream_socket_accept($socket)) {
  143. $fakeSMTP = new fakeSMTP($fd);
  144. $fakeSMTP->receive();
  145. }
  146. fclose($socket);