Skip to content

Commit 9bfe653

Browse files
committed
Fix IMAP server email in the reporting function and adding a reminder for SpamCop
1 parent 9aa671b commit 9bfe653

File tree

4 files changed

+202
-57
lines changed

4 files changed

+202
-57
lines changed

modules/core/handler_modules.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,6 @@ public function process() {
10231023

10241024
$new_settings = $this->get('new_user_settings', array());
10251025

1026-
// Helper function for validation
10271026
$set_setting = function($key, $value, $validator = null) use (&$new_settings) {
10281027
if ($validator && !$validator($value)) {
10291028
$new_settings[$key] = '';
@@ -1049,22 +1048,19 @@ public function process() {
10491048
$abuseipdb = $this->request->post['abuseipdb_settings'];
10501049
$new_settings['abuseipdb_enabled_setting'] = isset($abuseipdb['enabled']);
10511050

1052-
// Handle API key: if field is empty but api_key_set flag is present, preserve original key
1051+
// Handle API key
10531052
$api_key = $abuseipdb['api_key'] ?? '';
10541053
$api_key_was_set = isset($abuseipdb['api_key_set']) && $abuseipdb['api_key_set'] == '1';
10551054

10561055
if (empty($api_key) && $api_key_was_set) {
1057-
// User left field empty but key was originally set - preserve the original key
10581056
$original_key = $this->user_config->get('abuseipdb_api_key_setting', '');
10591057
if (!empty($original_key)) {
10601058
$new_settings['abuseipdb_api_key_setting'] = $original_key;
10611059
} else {
10621060
$new_settings['abuseipdb_api_key_setting'] = '';
10631061
}
10641062
} else {
1065-
// User entered a new value (or cleared it) - validate and set it
10661063
$set_setting('abuseipdb_api_key_setting', $api_key, function($v) {
1067-
// API key validation: non-empty string, reasonable length (10-200 chars)
10681064
return !empty($v) && strlen($v) >= 10 && strlen($v) <= 200;
10691065
});
10701066
}

modules/core/output_modules.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,8 +2411,6 @@ protected function output() {
24112411
$placeholder = $this->trans('Your AbuseIPDB API key');
24122412

24132413
if (!empty($api_key)) {
2414-
// Show empty field but indicate key is set via placeholder
2415-
// User must enter new value to change it, or leave empty to keep current
24162414
$placeholder = $this->trans('API key is set (••••••••) - enter new value to change');
24172415
}
24182416

modules/imap/functions.php

Lines changed: 188 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,9 +1719,11 @@ function normalize_spam_report_error($error_msg) {
17191719

17201720
/**
17211721
* Report spam message to SpamCop
1722+
* Uses authenticated SMTP to ensure proper SPF/DKIM validation
1723+
* Must use the exact email address from the IMAP server where the message is located
17221724
*/
17231725
if (!hm_exists('report_spam_to_spamcop')) {
1724-
function report_spam_to_spamcop($message_source, $reasons, $user_config) {
1726+
function report_spam_to_spamcop($message_source, $reasons, $user_config, $session = null, $imap_server_email = '') {
17251727
$spamcop_enabled = $user_config->get('spamcop_enabled_setting', false);
17261728
if (!$spamcop_enabled) {
17271729
return array('success' => false, 'error' => 'SpamCop reporting is not enabled');
@@ -1734,51 +1736,208 @@ function report_spam_to_spamcop($message_source, $reasons, $user_config) {
17341736

17351737
$sanitized_message = sanitize_message_for_spam_report($message_source, $user_config);
17361738

1737-
$from_email = $user_config->get('spamcop_from_email_setting', '');
1738-
if (empty($from_email)) {
1739-
// Try to get from IMAP servers
1740-
$imap_servers = $user_config->get('imap_servers', array());
1741-
if (!empty($imap_servers)) {
1742-
$first_server = reset($imap_servers);
1743-
$from_email = isset($first_server['user']) ? $first_server['user'] : '';
1739+
// SpamCop requires the exact email address associated with the account
1740+
$from_email = '';
1741+
if (!empty($imap_server_email)) {
1742+
$from_email = $imap_server_email;
1743+
} else {
1744+
// Fallback: try to get from spamcop_from_email_setting
1745+
$from_email = $user_config->get('spamcop_from_email_setting', '');
1746+
if (empty($from_email)) {
1747+
// or else get from the first IMAP server
1748+
$imap_servers = $user_config->get('imap_servers', array());
1749+
if (!empty($imap_servers)) {
1750+
$first_server = reset($imap_servers);
1751+
$from_email = isset($first_server['user']) ? $first_server['user'] : '';
1752+
}
17441753
}
17451754
}
17461755

17471756
if (empty($from_email)) {
17481757
return array('success' => false, 'error' => 'No sender email address configured');
17491758
}
17501759

1751-
$reasons_text = implode(', ', $reasons);
1760+
$subject = 'Spam report';
1761+
1762+
if (!class_exists('Hm_MIME_Msg')) {
1763+
$mime_file = (defined('APP_PATH') ? APP_PATH : dirname(__FILE__) . '/../') . 'modules/smtp/hm-mime-message.php';
1764+
if (file_exists($mime_file)) {
1765+
require_once $mime_file;
1766+
} else {
1767+
return array('success' => false, 'error' => 'SMTP module required for SpamCop reporting. Please enable the SMTP module.');
1768+
}
1769+
}
1770+
1771+
// Create temporary file for the spam message attachment
1772+
$file_dir = $user_config->get('attachment_dir', sys_get_temp_dir());
1773+
if (!is_dir($file_dir)) {
1774+
$file_dir = sys_get_temp_dir();
1775+
}
1776+
// Create subdirectory for user if using attachment_dir
1777+
if ($file_dir !== sys_get_temp_dir() && $session) {
1778+
$user_dir = $file_dir . DIRECTORY_SEPARATOR . md5($session->get('username', 'default'));
1779+
if (!is_dir($user_dir)) {
1780+
@mkdir($user_dir, 0755, true);
1781+
}
1782+
$file_dir = $user_dir;
1783+
}
1784+
$temp_file = tempnam($file_dir, 'spamcop_');
1785+
1786+
// format it like forward as attachment does
1787+
if (class_exists('Hm_Crypt') && class_exists('Hm_Request_Key')) {
1788+
$encrypted_content = Hm_Crypt::ciphertext($sanitized_message, Hm_Request_Key::generate());
1789+
file_put_contents($temp_file, $encrypted_content);
1790+
} else {
1791+
file_put_contents($temp_file, $sanitized_message);
1792+
}
1793+
1794+
// Build MIME message
1795+
$body = '';
1796+
$mime = new Hm_MIME_Msg($spamcop_email, $subject, $body, $from_email, false, '', '', '', '', $from_email);
1797+
1798+
$attachment = array(
1799+
'name' => 'spam.eml',
1800+
'type' => 'message/rfc822',
1801+
'size' => strlen($sanitized_message),
1802+
'filename' => $temp_file
1803+
);
1804+
1805+
$mime->add_attachments(array($attachment));
1806+
1807+
$mime_message = $mime->get_mime_msg();
17521808

1753-
$subject = 'Spam Report: ' . $reasons_text;
1809+
// SpamCop rejects automated submissions, so removed X-Mailer headers
1810+
$mime_message = preg_replace('/^X-Mailer:.*$/mi', '', $mime_message);
1811+
$mime_message = preg_replace('/\r\n\r\n+/', "\r\n\r\n", $mime_message); // Clean up extra blank lines
17541812

1755-
$body = "This email is being reported as spam for the following reasons:\n\n";
1756-
$body .= $reasons_text . "\n\n";
1757-
$body .= "--- Original Message ---\n\n";
1758-
$body .= $sanitized_message;
1813+
// Extract boundary and fix encoding (Hm_MIME_Msg uses 7bit for message/rfc822, SpamCop requires base64)
1814+
$parts = explode("\r\n\r\n", $mime_message, 2);
1815+
$mime_body = isset($parts[1]) ? $parts[1] : '';
1816+
1817+
// Extract boundary from body (Hm_MIME_Msg creates its own boundary)
1818+
$boundary = '';
1819+
if (preg_match('/^--([A-Za-z0-9]+)/m', $mime_body, $boundary_match)) {
1820+
$boundary = $boundary_match[1];
1821+
}
1822+
1823+
// Fix encoding from 7bit to base64 for message/rfc822 attachment
1824+
if (!empty($boundary)) {
1825+
$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';
1826+
1827+
if (preg_match($pattern, $mime_message, $matches)) {
1828+
$attachment_content = rtrim($matches[3], "\r\n");
1829+
$encoded_content = chunk_split(base64_encode($attachment_content));
1830+
$mime_message = preg_replace($pattern, '$1base64$2' . $encoded_content . '$4', $mime_message);
1831+
} elseif (defined('DEBUG_MODE') && DEBUG_MODE) {
1832+
Hm_Debug::add('SpamCop: Warning - Could not fix encoding from 7bit to base64', 'warning');
1833+
}
1834+
}
1835+
1836+
@unlink($temp_file);
1837+
1838+
$parts = explode("\r\n\r\n", $mime_message, 2);
1839+
$all_headers = isset($parts[0]) ? $parts[0] : '';
1840+
$mime_body = isset($parts[1]) ? $parts[1] : '';
1841+
1842+
// Extract boundary again if needed (after encoding fix)
1843+
if (empty($boundary) && preg_match('/^--([A-Za-z0-9]+)/m', $mime_body, $boundary_match)) {
1844+
$boundary = $boundary_match[1];
1845+
}
17591846

1760-
$timeout = 10; //dont foget to add it to UI
1847+
$headers = array();
1848+
$header_lines = explode("\r\n", $all_headers);
1849+
foreach ($header_lines as $line) {
1850+
if (preg_match('/^(From|Reply-To|MIME-Version|Content-Type):/i', $line)) {
1851+
if (preg_match('/^Content-Type:/i', $line) && !empty($boundary)) {
1852+
$headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
1853+
} else {
1854+
$headers[] = $line;
1855+
}
1856+
}
1857+
}
1858+
1859+
if (!class_exists('Hm_SMTP_List')) {
1860+
$smtp_file = (defined('APP_PATH') ? APP_PATH : dirname(__FILE__) . '/../') . 'modules/smtp/hm-smtp.php';
1861+
if (file_exists($smtp_file)) {
1862+
require_once $smtp_file;
1863+
}
1864+
}
1865+
1866+
if ($session !== null && class_exists('Hm_SMTP_List')) {
1867+
try {
1868+
Hm_SMTP_List::init($user_config, $session);
1869+
$smtp_servers = Hm_SMTP_List::dump();
1870+
$smtp_id = false;
1871+
foreach ($smtp_servers as $id => $server) {
1872+
if (isset($server['user']) && strtolower(trim($server['user'])) === strtolower(trim($from_email))) {
1873+
$smtp_id = $id;
1874+
break;
1875+
}
1876+
}
1877+
1878+
// if ($smtp_id === false && !empty($smtp_servers)) {
1879+
// $smtp_id = key($smtp_servers);
1880+
// }
1881+
1882+
if ($smtp_id !== false) {
1883+
$mailbox = Hm_SMTP_List::connect($smtp_id, false);
1884+
if ($mailbox && $mailbox->authed()) {
1885+
$smtp_headers = array();
1886+
$smtp_headers[] = 'From: ' . $from_email;
1887+
$smtp_headers[] = 'Reply-To: ' . $from_email;
1888+
$smtp_headers[] = 'To: ' . $spamcop_email;
1889+
$smtp_headers[] = 'Subject: ' . $subject;
1890+
$smtp_headers[] = 'MIME-Version: 1.0';
1891+
if (!empty($boundary)) {
1892+
$smtp_headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
1893+
}
1894+
$smtp_headers[] = 'Date: ' . date('r');
1895+
$smtp_headers[] = 'Message-ID: <' . md5(uniqid(rand(), true)) . '@' . php_uname('n') . '>';
1896+
1897+
$smtp_message = implode("\r\n", $smtp_headers) . "\r\n\r\n" . $mime_body;
1898+
1899+
$err_msg = $mailbox->send_message($from_email, array($spamcop_email), $smtp_message);
1900+
1901+
if ($err_msg === false) {
1902+
return array('success' => true);
1903+
} elseif (defined('DEBUG_MODE') && DEBUG_MODE) {
1904+
Hm_Debug::add(sprintf('SpamCop: SMTP send failed: %s', $err_msg), 'warning');
1905+
}
1906+
} elseif (defined('DEBUG_MODE') && DEBUG_MODE) {
1907+
Hm_Debug::add(sprintf('SpamCop: SMTP connection failed for server ID %s', $smtp_id), 'warning');
1908+
}
1909+
}
1910+
} catch (Exception $e) {
1911+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
1912+
Hm_Debug::add(sprintf('SpamCop: SMTP exception: %s', $e->getMessage()), 'error');
1913+
}
1914+
}
1915+
}
1916+
1917+
// Fallback to mail() if SMTP is not available
1918+
$timeout = 10;
17611919
$old_timeout = ini_get('default_socket_timeout');
17621920
ini_set('default_socket_timeout', $timeout);
17631921

17641922
try {
1765-
$headers = array();
1766-
$headers[] = 'From: ' . $from_email;
1767-
$headers[] = 'Reply-To: ' . $from_email;
1768-
$headers[] = 'X-Mailer: Cypht Spam Reporter';
1769-
$headers[] = 'Content-Type: message/rfc822';
1770-
1771-
$mail_sent = @mail($spamcop_email, $subject, $body, implode("\r\n", $headers));
1923+
$mail_sent = @mail($spamcop_email, $subject, $mime_body, implode("\r\n", $headers));
17721924

17731925
ini_set('default_socket_timeout', $old_timeout);
17741926

17751927
if ($mail_sent) {
17761928
return array('success' => true);
17771929
} else {
1778-
return array('success' => false, 'error' => 'Failed to send email to SpamCop');
1930+
$error = 'Failed to send email to SpamCop. Please ensure your server has valid SPF/DKIM records or configure an SMTP server.';
1931+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
1932+
Hm_Debug::add('SpamCop: mail() function failed', 'error');
1933+
}
1934+
return array('success' => false, 'error' => $error);
17791935
}
17801936
} catch (Exception $e) {
17811937
ini_set('default_socket_timeout', $old_timeout);
1938+
if (defined('DEBUG_MODE') && DEBUG_MODE) {
1939+
Hm_Debug::add(sprintf('SpamCop: Exception in mail(): %s', $e->getMessage()), 'error');
1940+
}
17821941
return array('success' => false, 'error' => $e->getMessage());
17831942
}
17841943
}}
@@ -1796,7 +1955,6 @@ function sanitize_message_for_spam_report($message_source, $user_config) {
17961955
}
17971956
}
17981957

1799-
// Split message into headers and body
18001958
$parts = explode("\r\n\r\n", $message_source, 2);
18011959
$headers = isset($parts[0]) ? $parts[0] : '';
18021960
$body = isset($parts[1]) ? $parts[1] : '';
@@ -1808,13 +1966,12 @@ function sanitize_message_for_spam_report($message_source, $user_config) {
18081966
}
18091967
}
18101968

1811-
// Remove sensitive headers
18121969
$sensitive_headers = array('X-Original-From', 'X-Forwarded-For', 'X-Real-IP');
18131970
foreach ($sensitive_headers as $header) {
18141971
$headers = preg_replace('/^' . preg_quote($header, '/') . ':.*$/mi', '', $headers);
18151972
}
18161973

1817-
// Clean up multiple blank lines
1974+
// Clean blank lines
18181975
$headers = preg_replace('/\r\n\r\n+/', "\r\n\r\n", $headers);
18191976

18201977
return $headers . "\r\n\r\n" . $body;
@@ -1828,42 +1985,34 @@ function sanitize_message_for_spam_report($message_source, $user_config) {
18281985
*/
18291986
if (!hm_exists('extract_ip_from_message')) {
18301987
function extract_ip_from_message($message_source) {
1831-
// Split message into headers and body
18321988
$parts = explode("\r\n\r\n", $message_source, 2);
18331989
$headers = isset($parts[0]) ? $parts[0] : '';
18341990

18351991
if (empty($headers)) {
18361992
return false;
18371993
}
1838-
1839-
// Parse headers into array, handling continuation lines
1994+
18401995
$header_lines = explode("\r\n", $headers);
18411996
$received_headers = array();
18421997
$current_header = '';
18431998

1844-
// Collect all Received headers (handling multi-line headers)
18451999
foreach ($header_lines as $line) {
18462000
if (preg_match('/^Received:/i', $line)) {
18472001
if (!empty($current_header)) {
18482002
$received_headers[] = $current_header;
18492003
}
18502004
$current_header = $line;
18512005
} elseif (!empty($current_header) && preg_match('/^\s+/', $line)) {
1852-
// Continuation line - append to current header
18532006
$current_header .= ' ' . trim($line);
18542007
} elseif (!empty($current_header)) {
1855-
// New header line - save current and reset
18562008
$received_headers[] = $current_header;
18572009
$current_header = '';
18582010
}
18592011
}
1860-
// Don't forget the last header
18612012
if (!empty($current_header)) {
18622013
$received_headers[] = $current_header;
18632014
}
1864-
1865-
// Collect all valid public IPs from Received headers
1866-
// Check in reverse order (last header = original sender, first header = last hop)
2015+
18672016
$valid_ips = array();
18682017

18692018
foreach (array_reverse($received_headers) as $received) {
@@ -1907,21 +2056,19 @@ function extract_ip_from_message($message_source) {
19072056
}
19082057
}
19092058

1910-
// Return first valid IP found (original sender, since we checked in reverse)
2059+
// THe original sender, will be the first valid founded since we checked in reverse
19112060
if (!empty($valid_ips)) {
19122061
return $valid_ips[0];
19132062
}
1914-
1915-
// Fallback: Check X-Originating-IP, X-Forwarded-For, X-Real-IP headers
2063+
19162064
$fallback_headers = array('X-Originating-IP', 'X-Forwarded-For', 'X-Real-IP');
19172065
foreach ($fallback_headers as $header_name) {
19182066
if (preg_match('/^' . preg_quote($header_name, '/') . ':\s*(.+)$/mi', $headers, $matches)) {
19192067
$ip = trim($matches[1]);
1920-
// Handle comma-separated IPs (take first)
19212068
if (strpos($ip, ',') !== false) {
19222069
$ip = trim(explode(',', $ip)[0]);
19232070
}
1924-
// Remove port if present (e.g., "192.168.1.1:8080")
2071+
// Remove port if present
19252072
if (strpos($ip, ':') !== false && !preg_match('/^\[.*\]$/', $ip)) {
19262073
$ip_parts = explode(':', $ip);
19272074
$ip = $ip_parts[0];
@@ -2015,7 +2162,6 @@ function report_spam_to_abuseipdb($message_source, $reasons, $user_config) {
20152162
if ($http_code === 200) {
20162163
$result = json_decode($response, true);
20172164
if (isset($result['data']['ipAddress'])) {
2018-
// Clear rate limit timestamp on success
20192165
$user_config->set($rate_limit_key, 0);
20202166
return array('success' => true);
20212167
} else {

0 commit comments

Comments
 (0)