diff --git a/CHANGELOG.md b/CHANGELOG.md index b694361..1e8dddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Update - Fixed issue with DNS test exclamation triangles buttons not showing up. - Fixed some compatibility issues with PHP 5.6 and 7.0. +- SPF, DMIK and DMARC are now detected properly. ## [3.1.2] - 2024-04-28 ### Updated diff --git a/locallib.php b/locallib.php index 7aa8c8c..9b5cb41 100644 --- a/locallib.php +++ b/locallib.php @@ -143,7 +143,9 @@ function local_mailtest_getuserip() { * @return string Message string. */ function local_mailtest_checkdns($domain) { - $message = '

' . get_string('checkingdomain', 'local_mailtest', $domain) . '

'; + global $CFG; + + $message = ''; $success = true; $xmark = ' '; @@ -152,79 +154,155 @@ function local_mailtest_checkdns($domain) { // Check SPF records. + $regex = '/^v=spf1( +([-+?~]?(all|include:(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|a(:(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?((\/(\d|1\d|2\d|3[0-2]))?(\/\/' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8]))?)?|mx(:(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?((\/(\d|1\d|2\d|3[0-2]))?' + . '(\/\/([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8]))?)?|ptr(:(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?|ip4:([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]' + . '|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]' + . '|25[0-5])(\/([0-9]|1[0-9]|2[0-9]|3[0-2]))?|ip6:(::|([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|' + . '([0-9A-Fa-f]{1,4}:){1,8}:|([0-9A-Fa-f]{1,4}:){7}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){6}(:' + . '[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){5}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){4}' + . '(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){3}(:[0-9A-Fa-f]{1,4}){1,5}|([0-9A-Fa-f]{1,4}:){2}' + . '(:[0-9A-Fa-f]{1,4}){1,6}|[0-9A-Fa-f]{1,4}:(:[0-9A-Fa-f]{1,4}){1,7}|:(:[0-9A-Fa-f]{1,4}){1,8}|' + . '([0-9A-Fa-f]{1,4}:){6}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|' + . '[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]' + . '|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){6}:' + . '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]' + . '|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?' + . '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]' + . '|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}([0-9]|[1-9][0-9]' + . '|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' + . '\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}([0-9]|[1-9][0-9]' + . '|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' + . '\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}([0-9]|[1-9][0-9]' + . '|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' + . '\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])|[0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}([0-9]|[1-9][0-9]' + . '|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' + . '\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}' + . '|2[0-4][0-9]|25[0-5])|::([0-9A-Fa-f]{1,4}:){0,6}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]' + . '|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]' + . '|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))' + . '(\/(\d{1,2}|10[0-9]|11[0-9]|12[0-8]))?|exists:(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))|redirect=(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|exp=(%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]' + . '|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]' + . '([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|[A-Za-z][-.0-9A-Z_a-z]*=' + . '(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|' + . '[!-$&-~])*))* *$/'; + // Perform DNS query for SPF TXT records. $spf = false; - $spfrecords = @dns_get_record($domain, DNS_TXT); - if (empty($dmarcrecords)) { - // No SPF records found. - $message .= $exclamation . get_string('spfnorecordfound', 'local_mailtest') . '
'; - } else { - // SPF records found. - $message .= $checkmark . get_string('spfrecordfound', 'local_mailtest') . '
'; - - // Check if it has the required tags. - foreach ($spfrecords as $record) { + $txtrecords = @dns_get_record($domain, DNS_TXT); + // If a TXT records is found, check if it contains SPF record. + if (!empty($txtrecords)) { + // Check if any have the required tags. + foreach ($txtrecords as $record) { if (strpos($record['txt'], 'v=spf1') !== false) { + // SPF record found. + $message .= $checkmark . get_string('spfrecordfound', 'local_mailtest') . '
'; + // Extract found SPF record data. $spfdata = $record['txt']; // Check if the SPF record contains at least one mechanism (mandatory). - if (preg_match('/^v=spf1(\s+\w+=\S+)+(\s+\w+)?$/', $spfdata)) { + if (preg_match($regex, $spfdata)) { // SPF record contains at least one mechanism, it's valid. $message .= $checkmark . get_string('spfvalidrecord', 'local_mailtest') . '
'; $spf = true; break; } - } - if (!$spf) { - $message .= $xmark . get_string('spfinvalidrecord', 'local_mailtest') . '
'; + if (!$spf) { + $message .= $xmark . get_string('spfinvalidrecord', 'local_mailtest') . '
'; + } } } } + // No SPF record was found. + if (!$spf && empty($message)) { + $message .= $exclamation . get_string('spfnorecordfound', 'local_mailtest') . '
'; + } // Check DKIM record. $dkim = false; - $dkimrecords = @dns_get_record("_domainkey." . $domain, DNS_TXT); - if (empty($dkimrecord)) { - // No DKIM records found. - $message .= $exclamation . get_string('dkimnorecordfound', 'local_mailtest') . '
'; + if (empty($emaildkimselector = $CFG->emaildkimselector)) { + $message .= $exclamation . get_string('dkimmissingselector', 'local_mailtest') . '
'; } else { - // DKIM records found. - $message .= $checkmark . get_string('dkimrecordfound', 'local_mailtest') . '
'; - - // Check if it has the required tags. - foreach ($dkimrecords as $record) { - // Extract DKIM record data. - $dkimdata = $record['txt']; - - // Check if the DKIM record contains all mandatory tags. - if ( - strpos($dkimdata, 'v=DKIM1') !== false && - strpos($dkimdata, 'k=') !== false && - strpos($dkimdata, 'p=') !== false - ) { - // DKIM record contains all mandatory tags, it's valid. - $message .= $checkmark . get_string('dkimvalidrecord', 'local_mailtest') . '
'; - $dkim = true; - break; + $txtrecords = @dns_get_record($emaildkimselector . '._domainkey.' . $domain, DNS_TXT); + + // If TXT records are found named *_domainkey, check if it contains DKIM record. + if (!empty($txtrecords)) { + // DKIM records found. + $message .= $checkmark . get_string('dkimrecordfound', 'local_mailtest') . '
'; + + // Check if it has the required tags. + foreach ($txtrecords as $record) { + // Extract DKIM record data. + $dkimdata = $record['txt']; + + // Check if the DKIM record contains all mandatory tags. + if ( + strpos($dkimdata, 'v=DKIM1') !== false && + strpos($dkimdata, 'k=') !== false && + strpos($dkimdata, 'p=') !== false + ) { + // DKIM record contains all mandatory tags, it's valid. + $message .= $checkmark . get_string('dkimvalidrecord', 'local_mailtest') . '
'; + $dkim = true; + break; + } } - } - if (!$dkim) { - $message .= $xmark . get_string('dkiminvalidrecord', 'local_mailtest') . '
'; - } else { - if (empty($CFG->emaildkimselector)) { - $message .= $exclamation . get_string('dkimmissingselector', 'local_mailtest') . '
'; + if (!$dkim) { + $message .= $xmark . get_string('dkiminvalidrecord', 'local_mailtest') . '
'; } else { - $message .= $checkmark . get_string('dkimselectorconfigured', 'local_mailtest') . '
'; + if (empty($CFG->emaildkimselector)) { + $message .= $exclamation . get_string('dkimmissingselector', 'local_mailtest') . '
'; + } else { + $message .= $checkmark . get_string('dkimselectorconfigured', 'local_mailtest') . '
'; + } + } + } else { + // Check to see if there might be a CNAME record instead. + $records = @dns_get_record($emaildkimselector . '._domainkey.' . $domain, DNS_CNAME); + $dkim = !empty($records); + if ($dkim) { + // DKIM CNAME type records can points to a DKIM key stored on another server. + $message .= $checkmark . get_string('dkimrecordfound', 'local_mailtest') . '
'; } } + + if (!$dkim) { + // No DKIM record was found. + $message .= $exclamation . get_string('dkimnorecordfound', 'local_mailtest') . '
'; + } } // Check DMARC records. - $dmarcrecords = @dns_get_record("_dmarc." . $domain, DNS_TXT); - if (empty($dmarcrecords)) { + $txtrecords = @dns_get_record('_dmarc.' . $domain, DNS_TXT); + if (empty($txtrecords)) { // No DMARC records found. $message .= $xmark . get_string('dmarcnorecordfound', 'local_mailtest') . '
'; $success = false; @@ -233,7 +311,7 @@ function local_mailtest_checkdns($domain) { $message .= $checkmark . get_string('dmarcrecordfound', 'local_mailtest') . '
'; // Check if it has the required tags. - foreach ($dmarcrecords as $record) { + foreach ($txtrecords as $record) { if ( preg_match('/v=DMARC1;/', $record['txt']) && preg_match('/p=(none|quarantine|reject);/', $record['txt']) @@ -294,8 +372,8 @@ function local_mailtest_checkdns($domain) { // Check BIMI record. // Perform DNS query for BIMI TXT records. - $bimirecords = @dns_get_record("_bimi." . $domain, DNS_TXT); - if (empty($bimirecords)) { + $txtrecords = @dns_get_record('_bimi.' . $domain, DNS_TXT); + if (empty($txtrecords)) { // Required tags not found in any of the DMARC records. $message .= $xmark . get_string('biminorecordfound', 'local_mailtest') . '
'; $success = false; @@ -304,7 +382,7 @@ function local_mailtest_checkdns($domain) { $message .= $checkmark . get_string('bimirecordfound', 'local_mailtest') . '
'; // Loop through each BIMI record. - foreach ($bimirecords as $record) { + foreach ($txtrecords as $record) { // Extract BIMI record data. $bimidata = $record['txt']; @@ -343,6 +421,7 @@ function local_mailtest_checkdns($domain) { . ' data-placement="right" data-content="

{message}

"' . ' data-html="true" tabindex="0" data-trigger="focus">' . ''; + $message = '

' . get_string('checkingdomain', 'local_mailtest', $domain) . '

' . $message; $message = str_replace('{message}', str_replace('"', '"', $message), $popupicon); return $message;