FakeSMTPHelper.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016 Daniel Calviño Sánchez <danxuliu@gmail.com>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. // Code below modified from https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/fakeSMTP.php,
  25. // which is under the MIT license (https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/LICENSE)
  26. /**
  27. * fakeSMTP - A PHP / inetd fake smtp server.
  28. * Allows client<->server interaction
  29. * The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt
  30. */
  31. class fakeSMTP {
  32. public $logFile = false;
  33. public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready';
  34. public function __construct($fd) {
  35. $this->mail = [];
  36. $this->mail['ipaddress'] = false;
  37. $this->mail['emailSender'] = '';
  38. $this->mail['emailRecipients'] = [];
  39. $this->mail['emailSubject'] = false;
  40. $this->mail['rawEmail'] = false;
  41. $this->mail['emailHeaders'] = false;
  42. $this->mail['emailBody'] = false;
  43. $this->fd = $fd;
  44. }
  45. public function receive() {
  46. $hasValidFrom = false;
  47. $hasValidTo = false;
  48. $receivingData = false;
  49. $header = true;
  50. $this->reply('220 '.$this->serverHello);
  51. $this->mail['ipaddress'] = $this->detectIP();
  52. while ($data = fgets($this->fd)) {
  53. $data = preg_replace('@\r\n@', "\n", $data);
  54. if (!$receivingData) {
  55. $this->log($data);
  56. }
  57. if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) {
  58. if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) {
  59. $this->mail['emailSender'] = $match[1];
  60. $this->reply('250 2.1.0 Ok');
  61. $hasValidFrom = true;
  62. } else {
  63. $this->reply('551 5.1.7 Bad sender address syntax');
  64. }
  65. } elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) {
  66. if (!$hasValidFrom) {
  67. $this->reply('503 5.5.1 Error: need MAIL command');
  68. } else {
  69. if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) {
  70. array_push($this->mail['emailRecipients'], $match[1]);
  71. $this->reply('250 2.1.5 Ok');
  72. $hasValidTo = true;
  73. } else {
  74. $this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]);
  75. }
  76. }
  77. } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) {
  78. $this->reply('250 2.0.0 Ok');
  79. $hasValidFrom = false;
  80. $hasValidTo = false;
  81. } elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) {
  82. $this->reply('250 2.0.0 Ok');
  83. } elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) {
  84. $this->reply('250 2.0.0 '.$match[1]);
  85. } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) {
  86. if (!$hasValidTo) {
  87. $this->reply('503 5.5.1 Error: need RCPT command');
  88. } else {
  89. $this->reply('354 Ok Send data ending with <CRLF>.<CRLF>');
  90. $receivingData = true;
  91. }
  92. } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) {
  93. $this->reply('250 HELO '.$this->mail['ipaddress']);
  94. } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) {
  95. break;
  96. } elseif (!$receivingData) {
  97. //~ $this->reply('250 Ok');
  98. $this->reply('502 5.5.2 Error: command not recognized');
  99. } elseif ($receivingData && $data == ".\n") {
  100. /* Email Received, now let's look at it */
  101. $receivingData = false;
  102. $this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10));
  103. $splitmail = explode("\n\n", $this->mail['rawEmail'], 2);
  104. if (count($splitmail) == 2) {
  105. $this->mail['emailHeaders'] = $splitmail[0];
  106. $this->mail['emailBody'] = $splitmail[1];
  107. $headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders']));
  108. $headerlines = explode("\n", $headers);
  109. for ($i = 0; $i < count($headerlines); $i++) {
  110. if (preg_match('/^Subject: (.*)/i', $headerlines[$i], $matches)) {
  111. $this->mail['emailSubject'] = trim($matches[1]);
  112. }
  113. }
  114. } else {
  115. $this->mail['emailBody'] = $splitmail[0];
  116. }
  117. set_time_limit(5); // Just run the exit to prevent open threads / abuse
  118. } elseif ($receivingData) {
  119. $this->mail['rawEmail'] .= $data;
  120. }
  121. }
  122. /* Say good bye */
  123. $this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']);
  124. fclose($this->fd);
  125. }
  126. public function log($s) {
  127. if ($this->logFile) {
  128. file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND);
  129. }
  130. }
  131. private function reply($s) {
  132. $this->log("REPLY:$s");
  133. fwrite($this->fd, $s . "\r\n");
  134. }
  135. private function detectIP() {
  136. $raw = explode(':', stream_socket_get_name($this->fd, true));
  137. return $raw[0];
  138. }
  139. private function validateEmail($email) {
  140. return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email));
  141. }
  142. private function generateRandom($length = 8) {
  143. $password = '';
  144. $possible = '2346789BCDFGHJKLMNPQRTVWXYZ';
  145. $maxlength = strlen($possible);
  146. $i = 0;
  147. for ($i = 0; $i < $length; $i++) {
  148. $char = substr($possible, mt_rand(0, $maxlength - 1), 1);
  149. if (!strstr($password, $char)) {
  150. $password .= $char;
  151. }
  152. }
  153. return $password;
  154. }
  155. }
  156. $socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr);
  157. if (!$socket) {
  158. exit();
  159. }
  160. while ($fd = stream_socket_accept($socket)) {
  161. $fakeSMTP = new fakeSMTP($fd);
  162. $fakeSMTP->receive();
  163. }
  164. fclose($socket);