AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of MailTest for Moodle - https://moodle.org/
//
// MailTest is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// MailTest is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with MailTest. If not, see <https://www.gnu.org/licenses/>.
/**
* Library of functions for MailTest.
*
* @package local_mailtest
* @copyright 2015-2024 TNG Consulting Inc. - www.tngconsulting.ca
* @author Michael Milette
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Generate a user info object based on provided parameters.
*
* @param string $email plain text email address.
* @param string $name (optional) plain text real name.
* @param int $id (optional) user ID
*
* @return object user info.
*/
function local_mailtest_generate_email_user($email, $name = '', $id = -99) {
$emailuser = new stdClass();
$emailuser->email = trim(filter_var($email, FILTER_SANITIZE_EMAIL));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$emailuser->email = '';
}
$name = format_text($name, FORMAT_HTML, ['trusted' => false, 'noclean' => false]);
$emailuser->firstname = trim(htmlspecialchars($name, ENT_COMPAT));
$emailuser->lastname = '';
$emailuser->maildisplay = true;
$emailuser->mailformat = 1; // 0 (zero) text-only emails, 1 (one) for HTML emails.
$emailuser->id = $id;
$emailuser->firstnamephonetic = '';
$emailuser->lastnamephonetic = '';
$emailuser->middlename = '';
$emailuser->alternatename = '';
return $emailuser;
}
/**
* Outputs a message box.
*
* @param string $text The text of the message.
* @param string $heading (optional) The text of the heading.
* @param int $level (optional) The level of importance of the
* heading. Default: 2.
* @param string $classes (optional) A space-separated list of CSS
* classes.
* @param string $link (optional) The link where you want the Continue
* button to take the user. Only displays the
* continue button if the link URL was specified.
* @param string $id (optional) An optional ID. Is applied to body
* instead of heading if no heading.
*/
function local_mailtest_msgbox($text, $heading = null, $level = 2, $classes = null, $link = null, $id = null) {
global $OUTPUT;
echo $OUTPUT->box_start(trim('box ' . $classes));
if (!is_null($heading)) {
echo $OUTPUT->heading($heading, $level, $id);
echo "<div>$text</div>" . PHP_EOL;
} else {
echo "<div id=\"$id\">$text</div>" . PHP_EOL;
}
if (!is_null($link)) {
echo $OUTPUT->continue_button($link);
}
echo $OUTPUT->box_end();
}
/**
* Get the user's public or private IP address.
*
* @return string Public IP address or the private IP address if the public address cannot be identified.
*/
function local_mailtest_getuserip() {
$fieldlist = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR',
'HTTP_CF_CONNECTING_IP',
'HTTP_X_CLUSTER_CLIENT_IP',
];
// Public range first.
$filterlist = [
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
];
foreach ($filterlist as $filter) {
foreach ($fieldlist as $field) {
if (!array_key_exists($field, $_SERVER) || empty($_SERVER[$field])) {
continue;
}
$iplist = explode(',', $_SERVER[$field]);
foreach ($iplist as $ip) {
// Strips off port number if it exists.
if (substr_count($ip, ':') == 1) {
// IPv4 with a port.
$ip = explode(':', $ip)[0];
} else if ($start = (substr($ip, 0, 1) == '[') && $end = strpos($ip, ']:') !== false) {
// IPv6 with a port.
$ip = substr($ip, $start + 1, $end - 2);
}
// Sanitize so that we only get public addresses.
$lastip = $ip; // But save other address just in case.
$ip = filter_var(trim($ip), FILTER_VALIDATE_IP, $filter);
if ($ip !== false) {
return($ip);
}
}
}
}
// Private or restricted range.
return $lastip;
}
/**
* Check DNS records for a given domain.
*
* This function checks the DKIM, SPF, DMARC, and BIMI records for a given domain.
* It returns an array with a success flag and a message string.
*
* @param string $domain The domain to check DNS records for.
* @return string Message string.
*/
function local_mailtest_checkdns($domain) {
$message = '<p class="alert alert-warning">' . get_string('checkingdomain', 'local_mailtest', $domain) . '</p>';
$success = true;
$xmark = '<i class="fa fa-circle-xmark text-danger" aria-hidden="true"></i> ';
$checkmark = '<i class="fa fa-check-circle text-success" aria-hidden="true"></i> ';
$exclamation = '<i class="fa fa-triangle-exclamation text-warning" aria-hidden="true"></i> ';
// Check SPF records.
// 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') . '<br>';
} else {
// SPF records found.
$message .= $checkmark . get_string('spfrecordfound', 'local_mailtest') . '<br>';
// Check if it has the required tags.
foreach ($spfrecords as $record) {
if (strpos($record['txt'], 'v=spf1') !== false) {
// 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)) {
// SPF record contains at least one mechanism, it's valid.
$message .= $checkmark . get_string('spfvalidrecord', 'local_mailtest') . '<br>';
$spf = true;
break;
}
}
if (!$spf) {
$message .= $xmark . get_string('spfinvalidrecord', 'local_mailtest') . '<br>';
}
}
}
// 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') . '<br>';
} else {
// DKIM records found.
$message .= $checkmark . get_string('dkimrecordfound', 'local_mailtest') . '<br>';
// 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') . '<br>';
$dkim = true;
break;
}
}
if (!$dkim) {
$message .= $xmark . get_string('dkiminvalidrecord', 'local_mailtest') . '<br>';
} else {
if (empty($CFG->emaildkimselector)) {
$message .= $exclamation . get_string('dkimmissingselector', 'local_mailtest') . '<br>';
} else {
$message .= $checkmark . get_string('dkimselectorconfigured', 'local_mailtest') . '<br>';
}
}
}
// Check DMARC records.
$dmarcrecords = @dns_get_record("_dmarc." . $domain, DNS_TXT);
if (empty($dmarcrecords)) {
// No DMARC records found.
$message .= $xmark . get_string('dmarcnorecordfound', 'local_mailtest') . '<br>';
$success = false;
} else {
// DMARC records found.
$message .= $checkmark . get_string('dmarcrecordfound', 'local_mailtest') . '<br>';
// Check if it has the required tags.
foreach ($dmarcrecords as $record) {
if (
preg_match('/v=DMARC1;/', $record['txt'])
&& preg_match('/p=(none|quarantine|reject);/', $record['txt'])
) {
// Required DMARC tags are present and valid.
$message .= $checkmark . get_string('dmarctagsfound', 'local_mailtest') . '<br>';
// Check rua tag if present.
$ruavalue = false;
if (preg_match('/rua=([^;]+)/', $record['txt'], $matches)) {
$ruavalue = $matches[1];
// Validate rua value format (should be a valid URI).
if (!filter_var($ruavalue, FILTER_VALIDATE_URL) && !filter_var("mailto:" . $ruavalue, FILTER_VALIDATE_EMAIL)) {
// The rua value is not formatted correctly.
$message .= $xmark . get_string('dmarcruainvalid', 'local_mailtest') . '<br>';
$success = false;
}
}
// Check ruf tag if present.
$rufvalue = false;
if (preg_match('/ruf=([^;]+)/', $record['txt'], $matches)) {
$rufvalue = $matches[1];
// Validate ruf value format (should be a valid URI).
if (!filter_var($rufvalue, FILTER_VALIDATE_URL) && !filter_var("mailto:" . $rufvalue, FILTER_VALIDATE_EMAIL)) {
// The ruf value is not formatted correctly.
$message .= $xmark . get_string('dmarcrufinvalid', 'local_mailtest') . '<br>';
$success = false;
}
}
// Check pct tag if present.
$pctvalue = false;
if (preg_match('/pct=([0-9]+)/', $record['txt'], $matches)) {
$pctvalue = intval($matches[1]);
// Validate pct value range (should be between 0 and 100).
if ($pctvalue < 0 || $pctvalue > 100) {
// The pct value is not within the valid range.
$message .= $xmark . get_string('dmarcpctinvalid', 'local_mailtest') . '<br>';
$success = false;
}
}
break;
} else {
// Required tags not found in any of the DMARC records.
$message .= $checkmark . get_string('dmarctagsfound', 'local_mailtest') . '<br>';
$success = false;
}
}
}
// Check to ensure that either DKIM or SPF is configured.
if (!$dkim && !$spf) {
$message .= $xmark . get_string('dkimspffailed', 'local_mailtest') . '<br>';
$success = false;
}
// Check BIMI record.
// Perform DNS query for BIMI TXT records.
$bimirecords = @dns_get_record("_bimi." . $domain, DNS_TXT);
if (empty($bimirecords)) {
// Required tags not found in any of the DMARC records.
$message .= $xmark . get_string('biminorecordfound', 'local_mailtest') . '<br>';
$success = false;
} else {
// Records found. Check if it has the required tags.
$message .= $checkmark . get_string('bimirecordfound', 'local_mailtest') . '<br>';
// Loop through each BIMI record.
foreach ($bimirecords as $record) {
// Extract BIMI record data.
$bimidata = $record['txt'];
// Check if BIMI record contains both v and l tags.
if (strpos($bimidata, 'v=BIMI1') !== false && strpos($bimidata, 'l=') !== false) {
// Required DMARC tags are present and valid.
$message .= $checkmark . get_string('bimitagsfound', 'local_mailtest') . '<br>';
// Extract logo URL from the BIMI record.
preg_match('/l=([^;]+)/', $bimidata, $matches);
$logourl = $matches[1];
// Validate existence of logo URL.
$headers = @get_headers($logourl);
if ($headers && strpos($headers[0], '200')) {
// Logo URL exists, BIMI record is valid.
$message .= $checkmark . get_string('bimiinvalidlogo', 'local_mailtest', $logourl) . '<br>';
break;
}
}
}
if (!$success) {
// Required tags not found in any of the DMARC records.
$message .= $xmark . get_string('bimidmarcfailure', 'local_mailtest') . '<br>';
$success = false;
}
if ($pctvalue != 100) {
$message .= $xmark . get_string('bimipctinvalid', 'local_mailtest') . '<br>';
$success = false;
}
}
$icon = $success ? 'fa-info-circle text-info' : 'fa-triangle-exclamation text-warning';
$title = get_string('iconlabel', 'local_mailtest', $domain);
$popupicon = '<a class="btn btn-link p-0" role="button" data-container="body" data-toggle="popover"'
. ' data-placement="right" data-content="<div class="no-overflow"><p>{message}</p></div>"'
. ' data-html="true" tabindex="0" data-trigger="focus">'
. '<i class="icon fa ' . $icon . ' fa-fw " title="' . $title . '" aria-label="' . $title . '"></i></a>';
$message = str_replace('{message}', str_replace('"', '"', $message), $popupicon);
return $message;
}