2020-05-28 21:52:11 +03:00
|
|
|
<?php
|
2020-08-24 15:54:25 +03:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*
|
2020-09-07 15:37:44 +03:00
|
|
|
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
2020-08-24 15:54:25 +03:00
|
|
|
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
|
|
|
|
*
|
|
|
|
* @license GNU AGPL version 3 or any later version
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
*/
|
2020-05-28 21:52:11 +03:00
|
|
|
// Code below modified from https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/fakeSMTP.php,
|
|
|
|
// which is under the MIT license (https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/LICENSE)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* fakeSMTP - A PHP / inetd fake smtp server.
|
|
|
|
* Allows client<->server interaction
|
|
|
|
* The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
class fakeSMTP {
|
|
|
|
public $logFile = false;
|
|
|
|
public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready';
|
|
|
|
|
|
|
|
public function __construct($fd) {
|
|
|
|
$this->mail = [];
|
|
|
|
$this->mail['ipaddress'] = false;
|
|
|
|
$this->mail['emailSender'] = '';
|
|
|
|
$this->mail['emailRecipients'] = [];
|
|
|
|
$this->mail['emailSubject'] = false;
|
|
|
|
$this->mail['rawEmail'] = false;
|
|
|
|
$this->mail['emailHeaders'] = false;
|
|
|
|
$this->mail['emailBody'] = false;
|
|
|
|
|
|
|
|
$this->fd = $fd;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function receive() {
|
|
|
|
$hasValidFrom = false;
|
|
|
|
$hasValidTo = false;
|
|
|
|
$receivingData = false;
|
|
|
|
$header = true;
|
|
|
|
$this->reply('220 '.$this->serverHello);
|
|
|
|
$this->mail['ipaddress'] = $this->detectIP();
|
|
|
|
while ($data = fgets($this->fd)) {
|
|
|
|
$data = preg_replace('@\r\n@', "\n", $data);
|
|
|
|
|
|
|
|
if (!$receivingData) {
|
|
|
|
$this->log($data);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) {
|
|
|
|
if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) {
|
|
|
|
$this->mail['emailSender'] = $match[1];
|
|
|
|
$this->reply('250 2.1.0 Ok');
|
|
|
|
$hasValidFrom = true;
|
|
|
|
} else {
|
|
|
|
$this->reply('551 5.1.7 Bad sender address syntax');
|
|
|
|
}
|
|
|
|
} elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) {
|
|
|
|
if (!$hasValidFrom) {
|
|
|
|
$this->reply('503 5.5.1 Error: need MAIL command');
|
|
|
|
} else {
|
|
|
|
if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) {
|
|
|
|
array_push($this->mail['emailRecipients'], $match[1]);
|
|
|
|
$this->reply('250 2.1.5 Ok');
|
|
|
|
$hasValidTo = true;
|
|
|
|
} else {
|
|
|
|
$this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) {
|
|
|
|
$this->reply('250 2.0.0 Ok');
|
|
|
|
$hasValidFrom = false;
|
|
|
|
$hasValidTo = false;
|
|
|
|
} elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) {
|
|
|
|
$this->reply('250 2.0.0 Ok');
|
|
|
|
} elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) {
|
|
|
|
$this->reply('250 2.0.0 '.$match[1]);
|
|
|
|
} elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) {
|
|
|
|
if (!$hasValidTo) {
|
|
|
|
$this->reply('503 5.5.1 Error: need RCPT command');
|
|
|
|
} else {
|
|
|
|
$this->reply('354 Ok Send data ending with <CRLF>.<CRLF>');
|
|
|
|
$receivingData = true;
|
|
|
|
}
|
|
|
|
} elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) {
|
|
|
|
$this->reply('250 HELO '.$this->mail['ipaddress']);
|
|
|
|
} elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) {
|
|
|
|
break;
|
|
|
|
} elseif (!$receivingData) {
|
|
|
|
//~ $this->reply('250 Ok');
|
|
|
|
$this->reply('502 5.5.2 Error: command not recognized');
|
|
|
|
} elseif ($receivingData && $data == ".\n") {
|
|
|
|
/* Email Received, now let's look at it */
|
|
|
|
$receivingData = false;
|
|
|
|
$this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10));
|
|
|
|
$splitmail = explode("\n\n", $this->mail['rawEmail'], 2);
|
|
|
|
if (count($splitmail) == 2) {
|
|
|
|
$this->mail['emailHeaders'] = $splitmail[0];
|
|
|
|
$this->mail['emailBody'] = $splitmail[1];
|
|
|
|
$headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders']));
|
|
|
|
$headerlines = explode("\n", $headers);
|
2020-10-05 16:12:57 +03:00
|
|
|
for ($i = 0; $i < count($headerlines); $i++) {
|
2020-05-28 21:52:11 +03:00
|
|
|
if (preg_match('/^Subject: (.*)/i', $headerlines[$i], $matches)) {
|
|
|
|
$this->mail['emailSubject'] = trim($matches[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->mail['emailBody'] = $splitmail[0];
|
|
|
|
}
|
|
|
|
set_time_limit(5); // Just run the exit to prevent open threads / abuse
|
|
|
|
} elseif ($receivingData) {
|
|
|
|
$this->mail['rawEmail'] .= $data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* Say good bye */
|
|
|
|
$this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']);
|
|
|
|
|
|
|
|
fclose($this->fd);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function log($s) {
|
|
|
|
if ($this->logFile) {
|
|
|
|
file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function reply($s) {
|
|
|
|
$this->log("REPLY:$s");
|
|
|
|
fwrite($this->fd, $s . "\r\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
private function detectIP() {
|
|
|
|
$raw = explode(':', stream_socket_get_name($this->fd, true));
|
|
|
|
return $raw[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
private function validateEmail($email) {
|
|
|
|
return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email));
|
|
|
|
}
|
|
|
|
|
2020-10-05 16:12:57 +03:00
|
|
|
private function generateRandom($length = 8) {
|
2020-05-28 21:52:11 +03:00
|
|
|
$password = '';
|
|
|
|
$possible = '2346789BCDFGHJKLMNPQRTVWXYZ';
|
|
|
|
$maxlength = strlen($possible);
|
|
|
|
$i = 0;
|
2020-10-05 16:12:57 +03:00
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
|
$char = substr($possible, mt_rand(0, $maxlength - 1), 1);
|
2020-05-28 21:52:11 +03:00
|
|
|
if (!strstr($password, $char)) {
|
|
|
|
$password .= $char;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $password;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr);
|
|
|
|
if (!$socket) {
|
|
|
|
exit();
|
|
|
|
}
|
|
|
|
|
|
|
|
while ($fd = stream_socket_accept($socket)) {
|
|
|
|
$fakeSMTP = new fakeSMTP($fd);
|
|
|
|
$fakeSMTP->receive();
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose($socket);
|