Skip to content

Instantly share code, notes, and snippets.

@ellisgl
Created May 7, 2023 17:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ellisgl/ab632f0f19474093e9e7b9adc1412ff6 to your computer and use it in GitHub Desktop.
Save ellisgl/ab632f0f19474093e9e7b9adc1412ff6 to your computer and use it in GitHub Desktop.
Simple SMTP server in PHP - Saves attachments - was used to get stuff from a network scanner.
<?php
include_once('vendor/autoload.php');
use ZBateson\MailMimeParser\Header\AddressHeader;
use ZBateson\MailMimeParser\MailMimeParser;
use ZBateson\MailMimeParser\Message\IMimePart;
const SMTP_RESP_220 = '220 localhost ESMTP PHP8.1 Ready';
const SMTP_RESP_221 = '221 localhost ESMTP PHP8.1 Service closing transmission channel';
const SMTP_RESP_250 = '250 OK';
const SMTP_RESP_354 = '354 Start mail input; end with <CRLF>.<CRLF>';
const SMTP_RESP_500 = '500 Syntax error, command unrecognized';
const SMTP_RESP_501 = '501 Syntax error in parameters or arguments';
/**
* Log to screen.
*
* @param string $message
*
* @return void
*/
function logIt(string $message): void
{
$dateTime = (new DateTime())->format(\DateTimeInterface::ATOM);
echo "[$dateTime] $message\n";
}
// Start listening on port 25
$socket = stream_socket_server('tcp://0.0.0.0:25', $errCode, $errStr);
if (!$socket) {
throw new RuntimeException($errStr, $errCode);
}
echo "Listening on port 25...\n";
$parser = new MailMimeParser();
while (1) {
// Accept incoming connections
$buffer = '';
$client = @stream_socket_accept($socket, 5);
if ($client) {
logIt("ACCEPTED: " . stream_socket_get_name($client, true));
// Send the greeting message.
fwrite($client, SMTP_RESP_220 . "\r\n");
while ($data = fread($client, 1024)) {
$buffer .= $data;
$pos = strpos($buffer, "\r\n");
if ($pos !== false) {
$line = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 2);
// Parse SMTP command
$command = strtoupper(substr($line, 0, 4));
$args = trim(substr($line, 4));
// Handle SMTP commands
switch ($command) {
case 'HELO':
case 'EHLO':
logit('GOT: HELO/EHLO');
fwrite($client, SMTP_RESP_250 . "\r\n");
break;
case 'MAIL':
logit('GOT: MAIL');
if (preg_match('/^FROM:\s*(<[^>]+>)/i', $args, $matches)) {
fwrite($client, SMTP_RESP_250 . "\r\n");
} else {
fwrite($client, SMTP_RESP_501 . "\r\n");
}
break;
case 'RCPT':
logIt('GOT: RCPT');
if (preg_match('/^TO:\s*(<[^>]+>)/i', $args, $matches)) {
fwrite($client, SMTP_RESP_250 . "\r\n");
} else {
fwrite($client, SMTP_RESP_501 . "\r\n");
}
break;
case 'DATA':
logIt('GOT: DATA');
fwrite($client, SMTP_RESP_354 . "\r\n");
// Create a temporary memory stream to hold the email data.
$tempStream = fopen('php://temp', 'r+');
// Copy the email data from the client socket stream to the temporary memory stream.
stream_copy_to_stream($client, $tempStream);
// Reset the temporary memory stream pointer to the beginning.
rewind($tempStream);
// Parse the email data incrementally using the MailMimeParser.
$message = $parser->parse($tempStream, true);
echo "{$message->getHeader('Message-ID')}\n";
echo "{$message->getHeader('Subject')}\n";
echo "{$message->getHeader('From')}\n";
/** @var AddressHeader | null $to */
$to = $message->getHeader('To');
foreach ($to->getAddresses() as $address) {
echo "TO: {$address->getName()} {$address->getEmail()}\n";
}
// Save all attachments.
/** @var IMimePart[] $attachments */
$attachments = $message->getAllAttachmentParts();
$attachmentCount = count($attachments);
$loopIndex = 1;
foreach ($attachments as $index => $part) {
$filename = $part->getHeaderParameter(
'Content-Type',
'name',
$part->getHeaderParameter(
'Content-Disposition',
'filename',
'__unknown_file_name_' . $index,
),
);
$out = fopen(__DIR__ . '/data/' . $filename, 'w+');
$str = $part->getBinaryContentResourceHandle();
stream_copy_to_stream($str, $out);
fclose($str);
fclose($out);
echo "ATTACHMENT $loopIndex: $filename\n";
++$loopIndex;
}
$response = "250 Message received\r\n";
fwrite($client, $response);
logIt('DATA FINISHED');
break;
case 'QUIT':
logIt('GOT: QUIT');
fwrite($client, SMTP_RESP_221 . "\r\n");
break;
default:
logIt("GOT: UNKNOWN [$line]");
$response = SMTP_RESP_500 . "\r\n";
fwrite($client, $response);
}
}
}
fclose($client);
logIt('Disconnected');
}
}
@ellisgl
Copy link
Author

ellisgl commented May 7, 2023

This was just a quick script. I'm working on making a full on library (that works) for this. This has spawned a couple other projects for other libraries for concepts that I haven't see before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment