Skip to content

Commit 47f083a

Browse files
committed
Implement APWG phishing reporting feature
1 parent 9bfe653 commit 47f083a

File tree

5 files changed

+286
-2
lines changed

5 files changed

+286
-2
lines changed

modules/core/handler_modules.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,15 @@ public function process() {
10431043
});
10441044
}
10451045

1046+
// Process APWG settings
1047+
if (array_key_exists('apwg_settings', $this->request->post)) {
1048+
$apwg = $this->request->post['apwg_settings'];
1049+
$new_settings['apwg_enabled_setting'] = isset($apwg['enabled']);
1050+
$set_setting('apwg_from_email_setting', $apwg['from_email'] ?? '', function($v) {
1051+
return filter_var($v, FILTER_VALIDATE_EMAIL);
1052+
});
1053+
}
1054+
10461055
// Process AbuseIPDB settings
10471056
if (array_key_exists('abuseipdb_settings', $this->request->post)) {
10481057
$abuseipdb = $this->request->post['abuseipdb_settings'];

modules/core/output_modules.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,6 +2396,39 @@ protected function output() {
23962396
}
23972397
}
23982398

2399+
/**
2400+
* Option to enable/disable APWG phishing reporting
2401+
* @subpackage core/output
2402+
*/
2403+
class Hm_Output_apwg_enabled_setting extends Hm_Output_Module {
2404+
protected function output() {
2405+
$settings = $this->get('user_settings', array());
2406+
$enabled = get_setting_value($settings, 'apwg_enabled', false);
2407+
$checked = $enabled ? ' checked="checked"' : '';
2408+
$reset = $enabled ? '<span class="tooltip_restore" restore_aria_label="Restore default value"><i class="bi bi-arrow-counterclockwise refresh_list reset_default_value_checkbox"></i></span>' : '';
2409+
2410+
return '<tr class="report_spam_setting"><td><label class="form-check-label" for="apwg_enabled">'.
2411+
$this->trans('Enable APWG phishing reporting').'</label></td>'.
2412+
'<td><input class="form-check-input" type="checkbox" '.$checked.' id="apwg_enabled" name="apwg_settings[enabled]" data-default-value="false" value="1" />'.$reset.'</td></tr>';
2413+
}
2414+
}
2415+
2416+
/**
2417+
* Option for APWG from email address
2418+
* @subpackage core/output
2419+
*/
2420+
class Hm_Output_apwg_from_email_setting extends Hm_Output_Module {
2421+
protected function output() {
2422+
$settings = $this->get('user_settings', array());
2423+
$email = get_setting_value($settings, 'apwg_from_email', '');
2424+
$reset = !empty($email) ? '<span class="tooltip_restore" restore_aria_label="Restore default value"><i class="bi bi-arrow-counterclockwise refresh_list reset_default_value_input"></i></span>' : '';
2425+
2426+
return '<tr class="report_spam_setting"><td><label for="apwg_from_email">'.
2427+
$this->trans('From email address (optional)').'</label></td>'.
2428+
'<td class="d-flex"><input class="form-control form-control-sm" type="email" id="apwg_from_email" name="apwg_settings[from_email]" value="'.$this->html_safe($email).'" placeholder="'.$this->trans('Uses your IMAP email if not set').'" />'.$reset.'</td></tr>';
2429+
}
2430+
}
2431+
23992432
/**
24002433
* Option for AbuseIPDB API key
24012434
* @subpackage core/output

modules/core/setup.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@
106106
add_output('settings', 'spamcop_enabled_setting', true, 'core', 'start_report_spam_settings', 'after');
107107
add_output('settings', 'spamcop_submission_email_setting', true, 'core', 'spamcop_enabled_setting', 'after');
108108
add_output('settings', 'spamcop_from_email_setting', true, 'core', 'spamcop_submission_email_setting', 'after');
109-
add_output('settings', 'abuseipdb_enabled_setting', true, 'core', 'spamcop_from_email_setting', 'after');
109+
add_output('settings', 'apwg_enabled_setting', true, 'core', 'spamcop_from_email_setting', 'after');
110+
add_output('settings', 'apwg_from_email_setting', true, 'core', 'apwg_enabled_setting', 'after');
111+
add_output('settings', 'abuseipdb_enabled_setting', true, 'core', 'apwg_from_email_setting', 'after');
110112
add_output('settings', 'abuseipdb_api_key_setting', true, 'core', 'abuseipdb_enabled_setting', 'after');
111113
add_output('settings', 'start_everything_settings', true, 'core', 'abuseipdb_api_key_setting', 'after');
112114
add_output('settings', 'all_since_setting', true, 'core', 'start_everything_settings', 'after');
@@ -368,6 +370,7 @@
368370
'images_whitelist' => FILTER_UNSAFE_RAW,
369371
'update' => FILTER_VALIDATE_BOOLEAN,
370372
'spamcop_settings' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FORCE_ARRAY),
373+
'apwg_settings' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FORCE_ARRAY),
371374
'abuseipdb_settings' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FORCE_ARRAY),
372375
)
373376
);

modules/imap/functions.php

Lines changed: 235 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1705,7 +1705,24 @@ function normalize_spam_report_error($error_msg) {
17051705
'AbuseIPDB validation error' => 'AbuseIPDB validation error. Please check your API key and configuration.',
17061706
'AbuseIPDB error' => 'An error occurred while reporting to AbuseIPDB. Please try again later.',
17071707
'Invalid response from AbuseIPDB' => 'Invalid response from AbuseIPDB. Please try again later.',
1708-
'cURL error' => 'Failed to connect to AbuseIPDB. Please check your internet connection.'
1708+
'cURL error' => 'Failed to connect to AbuseIPDB. Please check your internet connection.',
1709+
1710+
// APWG error mappings
1711+
'APWG reporting is not enabled' => 'APWG reporting is not enabled. Please enable it in Settings.',
1712+
'No sender email address configured' => 'No sender email address configured. Please configure it in Settings.',
1713+
'Failed to send email to APWG' => 'Failed to send email to APWG. Please check your server mail configuration.',
1714+
'send email to APWG' => 'Failed to send email to APWG. Please check your server mail configuration.',
1715+
'SMTP error' => 'Failed to send email to APWG. The SMTP server did not accept the message. Please check your SMTP configuration.',
1716+
'SMTP server did not confirm delivery' => 'Failed to send email to APWG. The SMTP server did not confirm delivery (expected 250 OK response). Please try again later.',
1717+
'SMTP server did not accept' => 'Failed to send email to APWG. The SMTP server did not accept the message for delivery. Please check your SMTP configuration.',
1718+
'RCPT command failed' => 'Failed to send email to APWG. The recipient address may be invalid or rejected by the SMTP server.',
1719+
'DATA command failed' => 'Failed to send email to APWG. The SMTP server did not accept the message data. Please try again later.',
1720+
'250' => 'Email was successfully sent to APWG (250 OK response received).',
1721+
'550' => 'Failed to send email to APWG. The recipient address was rejected by the mail server (550 error).',
1722+
'551' => 'Failed to send email to APWG. The recipient address does not exist (551 error).',
1723+
'552' => 'Failed to send email to APWG. The mail server rejected the message due to size limits (552 error).',
1724+
'553' => 'Failed to send email to APWG. The recipient address format is invalid (553 error).',
1725+
'554' => 'Failed to send email to APWG. The mail server rejected the message (554 error).'
17091726
);
17101727

17111728
foreach ($error_mappings as $key => $message) {
@@ -1942,6 +1959,223 @@ function report_spam_to_spamcop($message_source, $reasons, $user_config, $sessio
19421959
}
19431960
}}
19441961

1962+
/**
1963+
* Report phishing message to APWG (Anti-Phishing Working Group)
1964+
* Uses authenticated SMTP to ensure proper SPF/DKIM validation
1965+
* Must use the exact email address from the IMAP server where the message is located
1966+
*/
1967+
if (!hm_exists('report_spam_to_apwg')) {
1968+
function report_spam_to_apwg($message_source, $reasons, $user_config, $session = null, $imap_server_email = '') {
1969+
$apwg_enabled = $user_config->get('apwg_enabled_setting', false);
1970+
if (!$apwg_enabled) {
1971+
return array('success' => false, 'error' => 'APWG reporting is not enabled');
1972+
}
1973+
1974+
$apwg_email = '[email protected]';
1975+
1976+
$sanitized_message = sanitize_message_for_spam_report($message_source, $user_config);
1977+
1978+
$from_email = $user_config->get('apwg_from_email_setting', '');
1979+
if (empty($from_email)) {
1980+
$from_email = $imap_server_email;
1981+
}
1982+
1983+
if (empty($from_email)) {
1984+
return array('success' => false, 'error' => 'No sender email address configured');
1985+
}
1986+
1987+
$subject = 'Phishing Report';
1988+
1989+
if (!class_exists('Hm_MIME_Msg')) {
1990+
$mime_file = (defined('APP_PATH') ? APP_PATH : dirname(__FILE__) . '/../') . 'modules/smtp/hm-mime-message.php';
1991+
if (file_exists($mime_file)) {
1992+
require_once $mime_file;
1993+
} else {
1994+
return array('success' => false, 'error' => 'SMTP module required for APWG reporting. Please enable the SMTP module.');
1995+
}
1996+
}
1997+
1998+
$file_dir = $user_config->get('attachment_dir', sys_get_temp_dir());
1999+
if (!is_dir($file_dir)) {
2000+
$file_dir = sys_get_temp_dir();
2001+
}
2002+
2003+
if ($file_dir !== sys_get_temp_dir() && $session) {
2004+
$user_dir = $file_dir . DIRECTORY_SEPARATOR . md5($session->get('username', 'default'));
2005+
if (!is_dir($user_dir)) {
2006+
@mkdir($user_dir, 0755, true);
2007+
}
2008+
$file_dir = $user_dir;
2009+
}
2010+
$temp_file = tempnam($file_dir, 'apwg_');
2011+
2012+
if (class_exists('Hm_Crypt') && class_exists('Hm_Request_Key')) {
2013+
$encrypted_content = Hm_Crypt::ciphertext($sanitized_message, Hm_Request_Key::generate());
2014+
file_put_contents($temp_file, $encrypted_content);
2015+
} else {
2016+
file_put_contents($temp_file, $sanitized_message);
2017+
}
2018+
2019+
$body = '';
2020+
$mime = new Hm_MIME_Msg($apwg_email, $subject, $body, $from_email, false, '', '', '', '', $from_email);
2021+
2022+
$attachment = array(
2023+
'name' => 'phishing.eml',
2024+
'type' => 'message/rfc822',
2025+
'size' => strlen($sanitized_message),
2026+
'filename' => $temp_file
2027+
);
2028+
2029+
$mime->add_attachments(array($attachment));
2030+
2031+
$mime_message = $mime->get_mime_msg();
2032+
2033+
$mime_message = preg_replace('/\r\n\r\n+/', "\r\n\r\n", $mime_message);
2034+
2035+
$parts = explode("\r\n\r\n", $mime_message, 2);
2036+
$mime_body = isset($parts[1]) ? $parts[1] : '';
2037+
2038+
$boundary = '';
2039+
if (preg_match('/^--([A-Za-z0-9]+)/m', $mime_body, $boundary_match)) {
2040+
$boundary = $boundary_match[1];
2041+
}
2042+
2043+
if (!empty($boundary)) {
2044+
$pattern = '/(--' . preg_quote($boundary, '/') . '\r\nContent-Type: message\/rfc822[^\r\n]*\r\n(?:[^\r\n]*\r\n)*?Content-Transfer-Encoding: )7bit(\r\n\r\n)(.*?)(\r\n--' . preg_quote($boundary, '/') . '(?:--)?)/s';
2045+
2046+
if (preg_match($pattern, $mime_message, $matches)) {
2047+
$attachment_content = rtrim($matches[3], "\r\n");
2048+
$encoded_content = chunk_split(base64_encode($attachment_content));
2049+
$mime_message = preg_replace($pattern, '$1base64$2' . $encoded_content . '$4', $mime_message);
2050+
} elseif (defined('DEBUG_MODE') && DEBUG_MODE) {
2051+
Hm_Debug::add('APWG: Warning - Could not fix encoding from 7bit to base64', 'warning');
2052+
}
2053+
}
2054+
2055+
@unlink($temp_file);
2056+
2057+
$parts = explode("\r\n\r\n", $mime_message, 2);
2058+
$all_headers = isset($parts[0]) ? $parts[0] : '';
2059+
$mime_body = isset($parts[1]) ? $parts[1] : '';
2060+
2061+
if (empty($boundary) && preg_match('/^--([A-Za-z0-9]+)/m', $mime_body, $boundary_match)) {
2062+
$boundary = $boundary_match[1];
2063+
}
2064+
2065+
$headers = array();
2066+
$header_lines = explode("\r\n", $all_headers);
2067+
foreach ($header_lines as $line) {
2068+
if (preg_match('/^(From|Reply-To|MIME-Version|Content-Type):/i', $line)) {
2069+
if (preg_match('/^Content-Type:/i', $line) && !empty($boundary)) {
2070+
$headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
2071+
} else {
2072+
$headers[] = $line;
2073+
}
2074+
}
2075+
}
2076+
2077+
if (!class_exists('Hm_SMTP_List')) {
2078+
$smtp_file = (defined('APP_PATH') ? APP_PATH : dirname(__FILE__) . '/../') . 'modules/smtp/hm-smtp.php';
2079+
if (file_exists($smtp_file)) {
2080+
require_once $smtp_file;
2081+
}
2082+
}
2083+
2084+
if ($session !== null && class_exists('Hm_SMTP_List')) {
2085+
try {
2086+
Hm_SMTP_List::init($user_config, $session);
2087+
2088+
$smtp_servers = Hm_SMTP_List::dump();
2089+
$smtp_id = false;
2090+
foreach ($smtp_servers as $id => $server) {
2091+
if (isset($server['user']) && strtolower(trim($server['user'])) === strtolower(trim($from_email))) {
2092+
$smtp_id = $id;
2093+
break;
2094+
}
2095+
}
2096+
2097+
if ($smtp_id === false && !empty($smtp_servers)) {
2098+
$smtp_id = key($smtp_servers);
2099+
}
2100+
2101+
if ($smtp_id !== false) {
2102+
$mailbox = Hm_SMTP_List::connect($smtp_id, false);
2103+
if ($mailbox && $mailbox->authed()) {
2104+
$smtp_headers = array();
2105+
$smtp_headers[] = 'From: ' . $from_email;
2106+
$smtp_headers[] = 'Reply-To: ' . $from_email;
2107+
$smtp_headers[] = 'To: ' . $apwg_email;
2108+
$smtp_headers[] = 'Subject: ' . $subject;
2109+
$smtp_headers[] = 'MIME-Version: 1.0';
2110+
if (!empty($boundary)) {
2111+
$smtp_headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
2112+
}
2113+
$smtp_headers[] = 'Date: ' . date('r');
2114+
$smtp_headers[] = 'Message-ID: <' . md5(uniqid(rand(), true)) . '@' . php_uname('n') . '>';
2115+
2116+
$smtp_message = implode("\r\n", $smtp_headers) . "\r\n\r\n" . $mime_body;
2117+
2118+
$err_msg = $mailbox->send_message($from_email, array($apwg_email), $smtp_message);
2119+
2120+
if ($err_msg === false) {
2121+
// 250 OK response - APWG mail server accepted the email for delivery
2122+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2123+
Hm_Debug::add('APWG: Email accepted by SMTP server (250 OK)', 'info');
2124+
}
2125+
return array('success' => true);
2126+
} else {
2127+
// SMTP error - log the response for debugging
2128+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2129+
Hm_Debug::add(sprintf('APWG: SMTP send failed: %s', $err_msg), 'warning');
2130+
}
2131+
// Return error with SMTP response details
2132+
return array('success' => false, 'error' => sprintf('Failed to send email to APWG. SMTP error: %s', $err_msg));
2133+
}
2134+
} elseif (defined('DEBUG_MODE') && DEBUG_MODE) {
2135+
Hm_Debug::add(sprintf('APWG: SMTP connection failed for server ID %s', $smtp_id), 'warning');
2136+
}
2137+
}
2138+
} catch (Exception $e) {
2139+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2140+
Hm_Debug::add(sprintf('APWG: SMTP exception: %s', $e->getMessage()), 'error');
2141+
}
2142+
}
2143+
}
2144+
2145+
$timeout = 10;
2146+
$old_timeout = ini_get('default_socket_timeout');
2147+
ini_set('default_socket_timeout', $timeout);
2148+
2149+
try {
2150+
$mail_sent = @mail($apwg_email, $subject, $mime_body, implode("\r\n", $headers));
2151+
2152+
ini_set('default_socket_timeout', $old_timeout);
2153+
2154+
if ($mail_sent) {
2155+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2156+
Hm_Debug::add('APWG: mail() function returned true (delivery status unknown - no SMTP response available)', 'info');
2157+
}
2158+
return array('success' => true);
2159+
} else {
2160+
$error = 'Failed to send email to APWG. Please ensure your server has valid SPF/DKIM records or configure an SMTP server.';
2161+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2162+
Hm_Debug::add('APWG: mail() function returned false', 'error');
2163+
$last_error = error_get_last();
2164+
if ($last_error) {
2165+
Hm_Debug::add(sprintf('APWG: PHP error: %s', $last_error['message']), 'error');
2166+
}
2167+
}
2168+
return array('success' => false, 'error' => $error);
2169+
}
2170+
} catch (Exception $e) {
2171+
ini_set('default_socket_timeout', $old_timeout);
2172+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
2173+
Hm_Debug::add(sprintf('APWG: Exception in mail(): %s', $e->getMessage()), 'error');
2174+
}
2175+
return array('success' => false, 'error' => $e->getMessage());
2176+
}
2177+
}}
2178+
19452179
/**
19462180
* Sanitize message source for spam reporting
19472181
*/

modules/imap/handler_modules.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2236,13 +2236,18 @@ public function process() {
22362236
$services_to_report = array();
22372237
$service_names = array(
22382238
'spamcop' => 'SpamCop',
2239+
'apwg' => 'APWG',
22392240
'abuseipdb' => 'AbuseIPDB'
22402241
);
22412242

22422243
if ($this->user_config->get('spamcop_enabled_setting', false)) {
22432244
$services_to_report[] = 'spamcop';
22442245
}
22452246

2247+
if ($this->user_config->get('apwg_enabled_setting', false)) {
2248+
$services_to_report[] = 'apwg';
2249+
}
2250+
22462251
if ($this->user_config->get('abuseipdb_enabled_setting', false)) {
22472252
$services_to_report[] = 'abuseipdb';
22482253
}

0 commit comments

Comments
 (0)