src/ImapAbuseCheck.php
<?php
/**
* TF Related Functionality
* @author Joe Huss <detain@interserver.net>
* @copyright 2019
* @package MyAdmin
* @category Abuse
*/
use MyDb\Mysqli\Db;
use MyAdmin\Orm\Abuse;
use MyAdmin\Orm\Abuse_Data;
/**
* Handles checking IMAP email addresses looking through the emails for specific patterns indicating
* that an IP was blacklisted or being reported for abuse. it checks to see if the IP is one of ours,
* and finds the matching client and notifies them where appropriate
*/
class ImapAbuseCheck
{
public $imap_server;
public $imap_username;
public $imap_password;
public $imap_folder;
public $ip_regex = '(?P<ip>(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))';
public $delete_attachments;
public $mbox;
public $MC;
public $limit_ips = false;
public $ips = [];
public $mongo_client;
public $mb_db;
public $mb_users = [];
public $mb_ips = [];
public $emails = [];
public $abused = 0;
public $db;
public $all_ips;
public $client_ips;
public $preg_match = [];
public $preg_match_all = [];
public $email_headers;
// per message variables
public $charset;
public $htmlmsg;
public $plainmsg;
public $attachments;
/**
* @param string $imap_server the imap server {address:port/type}INBOX.mailbox , ie: {mx.interserver.net:143/imap}INBOX.Hotmail
* @param string $username the username for connecting to the imap server
* @param string $password the password for connecting to the imap server
* @param \Db $db the database handler
* @param int $delete_attachments whether or not to delete the attachments and emails
* @param bool|false|int $limit_ips how many types of spam should a person receive a day for this type before hitting a limit
*/
public function __construct($imap_server, $username, $password, $db, $delete_attachments = 1, $limit_ips = false)
{
$this->imap_server = $imap_server;
$this->imap_folder = preg_replace('/^{.*}/m', '', $this->imap_server);
$this->imap_username = $username;
$this->imap_password = $password;
$this->db = $db;
$this->set_default_email_headers();
$this->delete_attachments = $delete_attachments;
$this->limit_ips = $limit_ips;
if (isset($GLOBALS['abuse_ips'])) {
$this->ips = $GLOBALS['abuse_ips'];
}
//echo sizeof($this->ips) . " ips loaded, limiting to " . $this->limit_ips . " ips/address\n";
//print_r($this->ips);
if (isset($GLOBALS['all_ips'])) {
$this->all_ips = $GLOBALS['all_ips'];
} else {
$this->load_all_ips();
}
if (isset($GLOBALS['all_client_ips'])) {
$this->client_ips = $GLOBALS['all_client_ips'];
} else {
$this->load_client_ips();
}
$this->mb_db = new Db(ZONEMTA_MYSQL_DB, ZONEMTA_MYSQL_USERNAME, ZONEMTA_MYSQL_PASSWORD, ZONEMTA_MYSQL_HOST);
$this->mb_ips = explode("\n", trim(`grep address /home/sites/zone-mta/config/pools.js |cut -d\" -f4`));
try {
$this->mongo_client= new \MongoDB\Client('mongodb://'.ZONEMTA_USERNAME.':'.rawurlencode(ZONEMTA_PASSWORD).'@'.ZONEMTA_HOST.':27017/');
$this->mongo_users = $this->mongo_client->selectDatabase('zone-mta')->selectCollection('users');
$result = $this->mongo_users->find();
foreach ($result as $user) {
$this->mb_users[] = $user->username;
}
} catch (\Exception $e) {
myadmin_log('myadmin', 'error', 'MongoDB '.ZONEMTA_HOST.' down: '.$e->getMessage(), __LINE__, __FILE__);
}
$this->connect();
function_requirements('get_server_from_ip');
}
/**
* returns the ip regex string
*
* @return string the ip regex string
*/
public function get_ip_regex()
{
return $this->ip_regex;
}
/**
* sets the email headers to the default
*
* @return void
*/
public function set_default_email_headers()
{
$this->email_headers = "MIME-Version: 1.0\nContent-type: text/html; charset=UTF-8\nFrom: Abuse <abuse@interserver.net>\n";
}
/**
* @param $all_ips
*/
public function set_all_ips($all_ips)
{
$this->all_ips = $all_ips;
}
/**
* loads all the IP blocks into the class and global all_ips
*/
public function load_all_ips()
{
//echo "Loading IP Blocks\n";
function_requirements('get_all_ips_from_ipblocks');
$this->all_ips = get_all_ips_from_ipblocks(true);
$GLOBALS['all_ips'] = $this->all_ips;
}
/**
* loads client ips into class and global all_client_ips
*/
public function load_client_ips()
{
//echo "Loading IP Blocks\n";
function_requirements('get_client_ips');
$this->client_ips = get_client_ips(true);
$GLOBALS['all_client_ips'] = $this->client_ips;
}
/**
* connects to the imap server
*/
public function connect()
{
$this->mbox = imap_open($this->imap_server, $this->imap_username, $this->imap_password) or die('Cannot connect to '.$this->imap_server);
/* This Gave me this:
stdClass Object
(
[Date] => Thu, 27 Mar 2014 12:20:15 -0400 (EDT)
[Driver] => imap
[Mailbox] => {mx.interserver.net:143/imap/readonly/user="abuse1@interserver.net"}INBOX.cop
[Nmsgs] => 54
[Recent] => 21
)*/
$this->MC = imap_check($this->mbox);
echo "{$this->imap_folder} Got {$this->MC->Nmsgs} Messages".PHP_EOL;
}
/**
* registers a regular expression with the imap class
*
* @param string $regex
* @param string $against
* @param string $field
*/
public function register_preg_match($regex, $against = 'headers', $field = 'ip')
{
$regex = str_replace('%IP%', $this->get_ip_regex(), $regex);
$this->preg_match[] = [
'regex' => $regex,
'against' => $against,
'field' => $field
];
}
/**
* registers a preg_match_all type match with the imap class
*
* @param string $regex
* @param string $against
* @param string $field
*/
public function register_preg_match_all($regex, $against = 'headers', $field = 'ip')
{
$regex = str_replace('%IP%', $this->get_ip_regex(), $regex);
$this->preg_match_all[] = [
'regex' => $regex,
'against' => $against,
'field' => $field
];
}
/**
* @param string $type
* @param bool $limit
*/
public function process($type = 'spam', $limit = false)
{
//print_r($this->MC);
if ($this->MC->Nmsgs > 0) {
$abused = 0;
$db = $this->db;
if ($limit === false) {
$result = imap_fetch_overview($this->mbox, "1:{$this->MC->Nmsgs}", 0);
} else {
if ($limit > $this->MC->Nmsgs) {
$limit = $this->MC->Nmsgs;
}
$result = imap_fetch_overview($this->mbox, "1:{$limit}", 0);
}
foreach ($result as $overview) {
$this->getmsg($overview->msgno);
$subject = $overview->subject;
//echo "#{$overview->msgno} ({$overview->date}) - From: {$overview->from} {$overview->subject}\n";
$headers = imap_fetchbody($this->mbox, $overview->msgno, '0');
$body = imap_fetchbody($this->mbox, $overview->msgno, '1');
//echo $body.PHP_EOL;
//echo "Headers:\n$headers\n";
//echo "Body:\n$body\n";
$ip = false;
foreach ($this->preg_match as $match_data) {
if ($match_data['against'] == 'body') {
$match_against = $body;
} elseif ($match_data['against'] == 'bodyfull') {
$match_against = $this->plainmsg;
} else {
$match_against = $headers;
}
$match_res = preg_match($match_data['regex'], $match_against, $matches);
if ($match_res) {
if (trim($matches[$match_data['field']]) != '') {
$ip = trim($matches[$match_data['field']]);
}
} else {
//print_r($match_res);
//echo "{$this->imap_folder} Couldn't Find IP in " . $match_data['against'] . ":\n " . str_replace("\n", "\n ", $match_against) . "\nUsing " . $match_data['regex'].PHP_EOL;
}
}
foreach ($this->preg_match_all as $match_data) {
if ($match_data['against'] == 'body') {
$match_against = $body;
}
if ($match_data['against'] == 'bodyfull') {
$match_against = $this->plainmsg;
} else {
$match_against = $headers;
}
$match_res = preg_match_all($match_data['regex'], $match_against, $matches);
if ($match_res) {
if (is_array($matches[$match_data['field']]) && trim($matches[$match_data['field']][0]) != '') {
$ip = trim($matches[$match_data['field']][0]);
} elseif (trim($matches[$match_data['field']]) != '') {
$ip = trim($matches[$match_data['field']]);
}
} else {
//print_r($match_res);
//echo "{$this->imap_folder} Couldn't Find IP in {$match_data['against']}:\n " . str_replace("\n", "\n ", $match_against) . "\nUsing " . $match_data['regex'].PHP_EOL;
}
}
if ($ip !== false && validIp($ip, false) && (in_array($ip, $this->all_ips) || in_array($ip, $this->client_ips))) {
$mbUser = null;
$mbId = null;
if (in_array($ip, $this->client_ips)) {
$server_data = ['email' => 'sreekanth@nettlinxinc.com'];
} else {
$server_data = get_server_from_ip($ip);
}
if (in_array($ip, $this->mb_ips)) {
if (preg_match_all('/Authenticated sender: (?P<user>[^\)]*)\)/ms', $this->plainmsg, $matches) ||
preg_match_all('/smtp.auth=(?P<user>\S*)\s/ms', $this->plainmsg, $matches)) {
foreach ($matches['user'] as $user) {
if (in_array($user, $this->mb_users)) {
$mbUser = $db->real_escape($user);
$db->query("select * from mail where mail_username='{$mbUser}'");
if ($db->num_rows() > 0) {
$db->next_record(MYSQL_ASSOC);
$data = $GLOBALS['tf']->accounts->read($db->Record['mail_custid']);
$email = (!isset($data['email_abuse']) || trim($data['email_abuse']) == '') ? $data['account_lid'] : $data['email_abuse'];
$server_data = [
'email' => $email,
'status' => $db->Record['mail_status']
];
}
}
}
}
if (preg_match_all('/^ by (\S+|\S+ \(\S+\)) with (LMP|SMTP|ESMTP|ESMTPA|ESMTPS|ESMTPSA|HTTP) id (\S+)\.(\d{3})\s*$/mU', $this->plainmsg, $matches)) {
$ids = $matches[3];
foreach ($ids as $id) {
$id = $this->mb_db->real_escape($id);
$this->mb_db->query("select * from mail_messagestore where id='{$id}'");
if ($this->mb_db->num_rows() > 0) {
$mbId = $id;
}
}
}
}
if (mb_substr($ip, 0, 10) == '66.45.228.' || (isset($server_data['email']) && $server_data['email'] != '')) {
// if ($this->abused >= 5) exit;
$email = (!isset($server_data['email_abuse']) || is_null($server_data['email_abuse']) || trim($server_data['email_abuse']) == '' ? $server_data['email'] : $server_data['email_abuse']);
$subject = 'InterServer Abuse Report for '.$ip;
if (mb_substr($ip, 0, 10) == '66.45.228.') {
echo "{$this->imap_folder} Overwriting IP $ip Contact $email => abuse@interserver.net".PHP_EOL;
$email = 'abuse@interserver.net';
}
if ($email == 'sales@3shost.com') {
echo "{$this->imap_folder} Overwriting IP $ip Contact $email => abuse@interserver.net".PHP_EOL;
$email = 'abuse@interserver.net';
}
if ($email == 'john@interserver.net') {
$email = 'abuse@interserver.net';
}
//print_r(array('ip' => $ip, 'email' => $email, 'subject' => $subject, 'plainmsg' => $this->plainmsg, 'htmlmsg' => $this->htmlmsg));
//print_r(xml2array(trim($this->htmlmsg), 1, 'attribute'));
//exit;
$abuse = new Abuse($db);
$abuse->setTime(mysql_now())
->setIp($ip)
->setType($type)
->setAmount(1)
->setLid($email)
->setStatus('pending')
->setMbUser($mbUser)
->setMbId($mbId)
->save();
$id = $abuse->getId();
$abuseData = new Abuse_Data($db);
$abuseData->setId($id)
->setHeaders(trim(self::fix_headers($this->plainmsg.$this->htmlmsg)))
->setPlainmsg($this->plainmsg)
->setHtmlmsg($this->htmlmsg)
->save();
$email_template = file_get_contents(__DIR__.'/../../../../include/templates/email/client/abuse.tpl');
$message = str_replace(
['{$email}', '{$ip}', '{$type}', '{$count}', '{$id}', '{$key}'],
[$email, $ip, 'spam', 1, $id, md5("${id}${ip}${type}")],
$email_template
);
//$email = 'john@interserver.net';
if (!isset($this->ips[$ip])) {
$this->ips[$ip] = 0;
}
if (($this->limit_ips === false || $this->ips[$ip] < $this->limit_ips) && !in_array($server_data['status'], ['canceled', 'expired'])) {
echo "{$this->imap_folder} Abuse Entry for {$ip} Added - Emailing {$email}".PHP_EOL;
(new \MyAdmin\Mail())->clientMail($subject, $message, $email, 'client/abuse.tpl');
} else {
echo "{$this->imap_folder} Abuse Entry for {$ip} Added - Not Emailing {$email} ({$this->ips[$ip]} >= {$this->limit_ips} Limit)".PHP_EOL;
}
$this->ips[$ip]++;
$this->abused++;
} else {
//print_r($server_data);
echo "{$this->imap_folder} Error Finding Owner For {$ip}".PHP_EOL;
}
} else {
echo "{$this->imap_folder} Invalid IP {$ip} or not ours in Message Headers:" . json_encode(explode("\n", $headers)). "".PHP_EOL;
}
//echo "OVERVIEW:" . $overview->msgno . " " . $overview->subject . " " . $overview->date . "\nBODY:$body\n";
if ($this->delete_attachments == 1) {
imap_delete($this->mbox, $overview->msgno);
}
}
$GLOBALS['abuse_ips'] = $this->ips;
}
if ($this->delete_attachments == 1) {
$this->delete_messages();
}
$this->disconnect();
}
/**
* delete all the email messages in the given mailbox
*
* @return void
*/
public function delete_messages()
{
imap_expunge($this->mbox);
}
/**
* close connection to the imap server
*
* @return void
*/
public function disconnect()
{
imap_close($this->mbox);
}
/**
* loads the imap message and all its parts
*
* @param $mid
*/
public function getmsg($mid)
{
// input $mbox = IMAP stream, $mid = message id
// output all the following:
// htmlmsg, plainmsg, charset, attachments
/*
Got 2 Messages
mbox = 'Resource id #85';
msgno = '1';
charset = NULL;
htmlmsg = '';
plainmsg = '[ SpamCop V4.8.1.007 ]
This message is brief for your comfort. Please use links below for details.
';
attachments = array (
);
*/
$this->htmlmsg = $this->plainmsg = $this->charset = '';
$this->attachments = [];
// HEADER
$h = imap_headerinfo($this->mbox, $mid);
// add code here to get date, from, to, cc, subject...
// BODY
$s = imap_fetchstructure($this->mbox, $mid);
if (!isset($s->parts)) { // simple
$this->getpart($mid, $s, 0);
} // pass 0 as part-number
else { // multipart: cycle through each part
foreach ($s->parts as $partno0 => $p) {
$this->getpart($mid, $p, $partno0 + 1);
}
}
}
/**
* @param $mid
* @param $p
* @param $partno
*/
public function getpart($mid, $p, $partno)
{
// $partno = '1', '2', '2.1', '2.1.3', etc for multipart, 0 if simple
// DECODE DATA
$data = $partno ? imap_fetchbody($this->mbox, $mid, $partno) : // multipart
imap_body($this->mbox, $mid); // simple
// Any part may be encoded, even plain text messages, so check everything.
if ($p->encoding == 4) {
$data = quoted_printable_decode($data);
} elseif ($p->encoding == 3) {
$data = base64_decode($data);
}
// PARAMETERS
// get all parameters, like charset, filenames of attachments, etc.
$params = [];
if (isset($p->parameters)) {
foreach ($p->parameters as $x) {
$params[strtolower($x->attribute)] = $x->value;
}
}
if (isset($p->dparameters)) {
foreach ($p->dparameters as $x) {
$params[strtolower($x->attribute)] = $x->value;
}
}
// ATTACHMENT
// Any part with a filename is an attachment,
// so an attached text file (type 0) is not mistaken as the message.
if (isset($params['filename']) || isset($params['name'])) {
// filename may be given as 'Filename' or 'Name' or both
$filename = $params['filename'] ?? $params['name'];
// filename may be encoded, so see imap_mime_header_decode()
$this->attachments[$filename] = $data; // this is a problem if two files have same name
}
// TEXT
if ($p->type == 0 && $data) {
// Messages may be split in different parts because of inline attachments,
// so append parts together with blank row.
if (strtolower($p->subtype) == 'plain') {
$this->plainmsg .= trim($data) . "\n\n";
} else {
$this->htmlmsg .= $data.'<br><br>';
}
if (isset($params['charset'])) {
$this->charset = $params['charset'];
} // assume all parts are same charset
}
// EMBEDDED MESSAGE
// Many bounce notifications embed the original message as type 2,
// but AOL uses type 1 (multipart), which is not handled here.
// There are no PHP functions to parse embedded messages,
// so this just appends the raw source to the main message.
elseif ($p->type == 2 && $data) {
$this->plainmsg .= $data . "\n\n";
}
// SUBPART RECURSION
if (isset($p->parts)) {
foreach ($p->parts as $partno0 => $p2) {
$this->getpart($mid, $p2, $partno.'.'.($partno0 + 1));
} // 1.2, 1.2.1, etc.
}
}
/**
* displays the folders for an imap account
*/
public function get_folders()
{
/* This Gave me this:
(0) {mx.interserver.net:143/imap/readonly}INBOX.Archives.2009,'.',64<br />
...
(8) {mx.interserver.net:143/imap/readonly}INBOX.Archives.2012,'.',64<br />
(9) {mx.interserver.net:143/imap/readonly}INBOX.Comcast,'.',64<br />
(10) {mx.interserver.net:143/imap/readonly}INBOX.USA,'.',64<br />
(11) {mx.interserver.net:143/imap/readonly}INBOX.Archives.2008,'.',64<br />
(12) {mx.interserver.net:143/imap/readonly}INBOX.cop,'.',64<br />
*/
$list = imap_getmailboxes($this->mbox, $this->imap_server, '*');
if (is_array($list)) {
foreach ($list as $key => $val) {
echo "($key) " . __LINE__.PHP_EOL;
echo imap_utf7_decode($val->name).',';
echo "'" . $val->delimiter . "',";
echo $val->attributes . "<br />\n";
}
} else {
echo 'imap_getmailboxes failed: '.imap_last_error().PHP_EOL;
}
}
/**
* displays all he folders in the imap account
*/
public function list_folders()
{
/* This Gave me this:
<h1>Mailboxes</h1>
{mx.interserver.net:143/imap/readonly}INBOX.Archives.2009<br />
..
{mx.interserver.net:143/imap/readonly}INBOX.Comcast<br />
{mx.interserver.net:143/imap/readonly}INBOX.USA<br />
{mx.interserver.net:143/imap/readonly}INBOX.Archives.2008<br />
{mx.interserver.net:143/imap/readonly}INBOX.cop<br />
{mx.interserver.net:143/imap/readonly}INBOX.Archives.2010<br />
*/
echo "<h1>Mailboxes</h1>\n";
$folders = imap_list($this->mbox, $this->imap_server, '*');
if ($folders == false) {
echo "Call failed<br />\n";
} else {
foreach ($folders as $val) {
echo $val . "<br />".PHP_EOL;
}
}
}
public static function fix_headers($headers)
{
$out = '';
$state = 0;
$headers = false;
$lines = explode("\n", trim(str_replace("\r", '', $headers)));
foreach ($lines as $line) {
if ($state == 0) {
$out .= $line.PHP_EOL;
if (trim($line) == '') {
$state++;
}
} elseif ($state == 1) {
if (preg_match('/^[A-Z][a-zA-Z0-9\-]*: /', trim($line))) {
$headers = true;
$out .= $line.PHP_EOL;
$state--;
} elseif ($headers == true && trim($line) != '') {
$state++;
} elseif ($headers == false) {
$out .= $line.PHP_EOL;
}
}
}
$out = rtrim(preg_replace("/\n\s*\n/m", "\n", strip_tags($out)));
return $out;
}
}