PHP plugin for the MyAdmin control panel. Monitors IMAP mailboxes for abuse complaints, resolves offending IPs to customers, sends notifications.
composer install # install deps (PHPUnit ^9.6)
vendor/bin/phpunit # run all tests (config: phpunit.xml.dist)
vendor/bin/phpunit --coverage-text # text coverage reportNamespace: Detain\MyAdminAbuse\ → src/ (PSR-4 via composer.json)
Test namespace: Detain\MyAdminAbuse\Tests\ → tests/ (bootstrap: tests/bootstrap.php)
Source: src/Plugin.php · src/ImapAbuseCheck.php · src/abuse.php · src/abuse_admin.php
src/Plugin.php— registerssystem.settings,ui.menu,function.requirementshookssrc/ImapAbuseCheck.php— IMAP processor; parses emails, extracts IPs, writesabuse/abuse_datatables; no namespace (loaded viaadd_requirement)src/abuse.php— client-facing page: view/respond to complaint (token or session auth)src/abuse_admin.php— admin dashboard: stats, manual report, UCEProtect/Trend Micro CSV import
Bin scripts (cron): bin/abuse_imap_downloader.php · bin/match_abuse.php · bin/clean_up_abuse_headers.php
bin/abuse_imap_downloader.php— readsINCLUDE_ROOT/config/abuse.json, runsImapAbuseCheck->process()bin/match_abuse.php— correlatesabuse_ipwith MailBaby/ZoneMTA users via MongoDB +ZONEMTA_*constantsbin/clean_up_abuse_headers.php— batch-cleansabuse_headersviaImapAbuseCheck::fix_headers()
src/Plugin.php — all handler methods are public static function name(GenericEvent $event):
public static function getHooks(): array {
return [
'system.settings' => [__CLASS__, 'getSettings'],
'ui.menu' => [__CLASS__, 'getMenu'],
'function.requirements'=> [__CLASS__, 'getRequirements'],
];
}
// Register pages/classes in getRequirements():
$loader->add_page_requirement('abuse', '/src/abuse.php');
$loader->add_requirement('class.ImapAbuseCheck', '/src/ImapAbuseCheck.php');Never use PDO. Use the MyAdmin DB wrapper from get_module_db():
$db = get_module_db('default'); // or 'mail' for mail module
$db->query("SELECT * FROM abuse WHERE abuse_id={$id}", __LINE__, __FILE__);
$db->next_record(MYSQL_ASSOC);
$row = $db->Record;
// Inserts: always make_insert_query()
$db->query(make_insert_query('abuse', [
'abuse_id' => null,
'abuse_time' => mysql_now(),
'abuse_ip' => $ip,
'abuse_type' => 'spam', // scanning|hacking|spam|child porn|phishing site|other
'abuse_amount' => 1,
'abuse_lid' => $email,
'abuse_status' => 'pending',
]), __LINE__, __FILE__);
$id = $db->getLastInsertId('abuse', 'abuse_id');
$db->query(make_insert_query('abuse_data', ['abuse_id' => $id, 'abuse_headers' => ImapAbuseCheck::fix_headers($headers)]), __LINE__, __FILE__);
$db->real_escape($userInput); // escape all user inputConfig from INCLUDE_ROOT/config/abuse.json. Pattern used in bin/abuse_imap_downloader.php:
$abuse = new ImapAbuseCheck(
'{'.$check['host'].':'.$check['port'].'/imap/ssl}'.$check['mailbox'],
ABUSE_IMAP_USER, ABUSE_IMAP_PASS, $db,
$check['delete_attachments'], $check['mail_limit']
);
$abuse->register_preg_match('/pattern with %IP%/', 'headers', 'ip'); // single match
$abuse->register_preg_match_all('/pattern/', 'body', 'ip'); // all matches
$abuse->process('spam', 100);
// Static helper — strips HTML, collapses blank lines:
$clean = ImapAbuseCheck::fix_headers($rawHeaders);- Admin pages: always check
$GLOBALS['tf']->ima == 'admin'ANDhas_acl('client_billing') - CSRF:
verify_csrf('abuse_admin')per form — tokens:abuse_admin,abuse_admin_multiple,abuse_admin_uce,abuse_admin_trend - Validate IPs:
validIp($ip, false)beforeget_server_from_ip($ip) - Escape:
$db->real_escape()on all$_GET/$_POSTvalues - Ownership check in
src/abuse.php: verify$server_data['email'] == $accounts->data['account_lid']before showing complaint - Email notifications: template at
include/templates/email/client/abuse.tplvia(new \MyAdmin\Mail())->clientMail()
Tests in tests/ — PHPUnit 9 (phpunit.xml.dist), bootstrap tests/bootstrap.php:
tests/FileExistenceTest.php— assertssrc/andbin/files existtests/PluginTest.php— reflection tests forPluginhooks, static props, handler signaturestests/ImapAbuseCheckTest.php— reflection tests for properties, methods,ip_regexdefaultstests/SourceCodeAnalysisTest.php—file_get_contentschecks for required source patterns
Reflection test pattern (no DB, no IMAP needed):
$ref = new ReflectionClass(\Detain\MyAdminAbuse\Plugin::class);
$method = $ref->getMethod('getHooks');
$this->assertTrue($method->isPublic() && $method->isStatic());- Commit messages: lowercase, descriptive (
fix imap header parsing,add uceprotect import) - Tabs for indentation (per
.scrutinizer.ymlcoding style config) ImapAbuseCheckhas no namespace — it is a global class loaded viafunction_requirements('class.ImapAbuseCheck')- Log with
myadmin_log('myadmin', 'debug', $msg, __LINE__, __FILE__) - Run
caliber refreshbefore committing; stageCLAUDE.md .claude/ CALIBER_LEARNINGS.md
IMPORTANT: Before every git commit, you MUST ensure Caliber syncs agent configs with the latest code changes.
First, check if the pre-commit hook is already installed:
grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"- If hook-active: the hook handles sync automatically — just commit normally. Tell the user: "Caliber will sync your agent configs automatically via the pre-commit hook."
- If no-hook: run Caliber manually before committing:
- Tell the user: "Caliber: Syncing agent configs with your latest changes..."
- Run:
caliber refresh && git add CLAUDE.md .claude/ .cursor/ .cursorrules .github/copilot-instructions.md .github/instructions/ AGENTS.md CALIBER_LEARNINGS.md .agents/ .opencode/ 2>/dev/null - After it completes, briefly tell the user what Caliber updated. Then proceed with the commit.
If caliber is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
Read CALIBER_LEARNINGS.md for patterns and anti-patterns learned from previous sessions.
These are auto-extracted from real tool usage — treat them as project-specific rules.
This project uses Caliber to keep AI agent configs in sync across Claude Code, Cursor, Copilot, and Codex.
Configs update automatically before each commit via caliber refresh.
If the pre-commit hook is not set up, run /setup-caliber to configure everything automatically.