@@ -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 */
17231725if (!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 */
18291986if (!hm_exists ('extract_ip_from_message ' )) {
18301987function 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