Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of MailTest for Moodle - https://moodle.org/
3
//
4
// MailTest is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// MailTest is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with MailTest.  If not, see <https://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Library of functions for MailTest.
19
 *
20
 * @package    local_mailtest
21
 * @copyright  2015-2024 TNG Consulting Inc. - www.tngconsulting.ca
22
 * @author     Michael Milette
23
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
/**
27
 * Generate a user info object based on provided parameters.
28
 *
29
 * @param      string  $email  plain text email address.
30
 * @param      string  $name   (optional) plain text real name.
31
 * @param      int     $id     (optional) user ID
32
 *
33
 * @return     object  user info.
34
 */
35
function local_mailtest_generate_email_user($email, $name = '', $id = -99) {
36
    $emailuser = new stdClass();
37
    $emailuser->email = trim(filter_var($email, FILTER_SANITIZE_EMAIL));
38
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
39
        $emailuser->email = '';
40
    }
41
    $name = format_text($name, FORMAT_HTML, ['trusted' => false, 'noclean' => false]);
42
    $emailuser->firstname = trim(htmlspecialchars($name, ENT_COMPAT));
43
    $emailuser->lastname = '';
44
    $emailuser->maildisplay = true;
45
    $emailuser->mailformat = 1; // 0 (zero) text-only emails, 1 (one) for HTML emails.
46
    $emailuser->id = $id;
47
    $emailuser->firstnamephonetic = '';
48
    $emailuser->lastnamephonetic = '';
49
    $emailuser->middlename = '';
50
    $emailuser->alternatename = '';
51
    return $emailuser;
52
}
53
 
54
/**
55
 * Outputs a message box.
56
 *
57
 * @param      string  $text     The text of the message.
58
 * @param      string  $heading  (optional) The text of the heading.
59
 * @param      int     $level    (optional) The level of importance of the
60
 *                               heading. Default: 2.
61
 * @param      string  $classes  (optional) A space-separated list of CSS
62
 *                               classes.
63
 * @param      string  $link     (optional) The link where you want the Continue
64
 *                               button to take the user. Only displays the
65
 *                               continue button if the link URL was specified.
66
 * @param      string  $id       (optional) An optional ID. Is applied to body
67
 *                               instead of heading if no heading.
68
 */
69
function local_mailtest_msgbox($text, $heading = null, $level = 2, $classes = null, $link = null, $id = null) {
70
    global $OUTPUT;
71
    echo $OUTPUT->box_start(trim('box ' . $classes));
72
    if (!is_null($heading)) {
73
        echo $OUTPUT->heading($heading, $level, $id);
74
        echo "<div>$text</div>" . PHP_EOL;
75
    } else {
76
        echo "<div id=\"$id\">$text</div>" . PHP_EOL;
77
    }
78
    if (!is_null($link)) {
79
        echo $OUTPUT->continue_button($link);
80
    }
81
    echo $OUTPUT->box_end();
82
}
83
 
84
/**
85
 * Get the user's public or private IP address.
86
 *
87
 * @return     string  Public IP address or the private IP address if the public address cannot be identified.
88
 */
89
function local_mailtest_getuserip() {
90
    $fieldlist = [
91
        'HTTP_CLIENT_IP',
92
        'HTTP_X_FORWARDED_FOR',
93
        'HTTP_X_FORWARDED',
94
        'HTTP_FORWARDED_FOR',
95
        'HTTP_FORWARDED',
96
        'REMOTE_ADDR',
97
        'HTTP_CF_CONNECTING_IP',
98
        'HTTP_X_CLUSTER_CLIENT_IP',
99
    ];
100
 
101
    // Public range first.
102
    $filterlist = [
103
        FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
104
        FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
105
    ];
106
 
107
    foreach ($filterlist as $filter) {
108
        foreach ($fieldlist as $field) {
109
            if (!array_key_exists($field, $_SERVER) || empty($_SERVER[$field])) {
110
                continue;
111
            }
112
 
113
            $iplist = explode(',', $_SERVER[$field]);
114
            foreach ($iplist as $ip) {
115
                // Strips off port number if it exists.
116
                if (substr_count($ip, ':') == 1) {
117
                    // IPv4 with a port.
118
                    $ip = explode(':', $ip)[0];
119
                } else if ($start = (substr($ip, 0, 1) == '[') && $end = strpos($ip, ']:') !== false) {
120
                    // IPv6 with a port.
121
                    $ip = substr($ip, $start + 1, $end - 2);
122
                }
123
                // Sanitize so that we only get public addresses.
124
                $lastip = $ip; // But save other address just in case.
125
                $ip = filter_var(trim($ip), FILTER_VALIDATE_IP, $filter);
126
                if ($ip !== false) {
127
                    return($ip);
128
                }
129
            }
130
        }
131
    }
132
    // Private or restricted range.
133
    return $lastip;
134
}
135
 
136
/**
137
 * Check DNS records for a given domain.
138
 *
139
 * This function checks the DKIM, SPF, DMARC, and BIMI records for a given domain.
140
 * It returns an array with a success flag and a message string.
141
 *
142
 * @param string $domain The domain to check DNS records for.
143
 * @return string Message string.
144
 */
145
function local_mailtest_checkdns($domain) {
146
    $message = '<p class="alert alert-warning">' . get_string('checkingdomain', 'local_mailtest', $domain) . '</p>';
147
    $success = true;
148
 
149
    $xmark = '<i class="fa fa-circle-xmark text-danger" aria-hidden="true"></i> ';
150
    $checkmark = '<i class="fa fa-check-circle text-success" aria-hidden="true"></i> ';
151
    $exclamation = '<i class="fa fa-triangle-exclamation text-warning" aria-hidden="true"></i> ';
152
 
153
    // Check SPF records.
154
 
155
    // Perform DNS query for SPF TXT records.
156
    $spf = false;
157
    $spfrecords = @dns_get_record($domain, DNS_TXT);
158
    if (empty($dmarcrecords)) {
159
        // No SPF records found.
160
        $message .= $exclamation . get_string('spfnorecordfound', 'local_mailtest')  . '<br>';
161
    } else {
162
        // SPF records found.
163
        $message .= $checkmark . get_string('spfrecordfound', 'local_mailtest')  . '<br>';
164
 
165
        // Check if it has the required tags.
166
        foreach ($spfrecords as $record) {
167
            if (strpos($record['txt'], 'v=spf1') !== false) {
168
                // Extract found SPF record data.
169
                $spfdata = $record['txt'];
170
 
171
                // Check if the SPF record contains at least one mechanism (mandatory).
172
                if (preg_match('/^v=spf1(\s+\w+=\S+)+(\s+\w+)?$/', $spfdata)) {
173
                    // SPF record contains at least one mechanism, it's valid.
174
                    $message .= $checkmark . get_string('spfvalidrecord', 'local_mailtest')  . '<br>';
175
                    $spf = true;
176
                    break;
177
                }
178
            }
179
            if (!$spf) {
180
                $message .= $xmark . get_string('spfinvalidrecord', 'local_mailtest')  . '<br>';
181
            }
182
        }
183
    }
184
 
185
    // Check DKIM record.
186
 
187
    $dkim = false;
188
    $dkimrecords = @dns_get_record("_domainkey." . $domain, DNS_TXT);
189
    if (empty($dkimrecord)) {
190
        // No DKIM records found.
191
        $message .= $exclamation . get_string('dkimnorecordfound', 'local_mailtest')  . '<br>';
192
    } else {
193
        // DKIM records found.
194
        $message .= $checkmark . get_string('dkimrecordfound', 'local_mailtest')  . '<br>';
195
 
196
        // Check if it has the required tags.
197
        foreach ($dkimrecords as $record) {
198
            // Extract DKIM record data.
199
            $dkimdata = $record['txt'];
200
 
201
            // Check if the DKIM record contains all mandatory tags.
202
            if (
203
                strpos($dkimdata, 'v=DKIM1') !== false &&
204
                strpos($dkimdata, 'k=') !== false &&
205
                strpos($dkimdata, 'p=') !== false
206
            ) {
207
                // DKIM record contains all mandatory tags, it's valid.
208
                $message .= $checkmark . get_string('dkimvalidrecord', 'local_mailtest')  . '<br>';
209
                $dkim = true;
210
                break;
211
            }
212
        }
213
        if (!$dkim) {
214
            $message .= $xmark . get_string('dkiminvalidrecord', 'local_mailtest')  . '<br>';
215
        } else {
216
            if (empty($CFG->emaildkimselector)) {
217
                $message .= $exclamation . get_string('dkimmissingselector', 'local_mailtest')  . '<br>';
218
            } else {
219
                $message .= $checkmark . get_string('dkimselectorconfigured', 'local_mailtest')  . '<br>';
220
            }
221
        }
222
    }
223
 
224
    // Check DMARC records.
225
 
226
    $dmarcrecords = @dns_get_record("_dmarc." . $domain, DNS_TXT);
227
    if (empty($dmarcrecords)) {
228
        // No DMARC records found.
229
        $message .= $xmark . get_string('dmarcnorecordfound', 'local_mailtest')  . '<br>';
230
        $success = false;
231
    } else {
232
        // DMARC records found.
233
        $message .= $checkmark . get_string('dmarcrecordfound', 'local_mailtest')  . '<br>';
234
 
235
        // Check if it has the required tags.
236
        foreach ($dmarcrecords as $record) {
237
            if (
238
                preg_match('/v=DMARC1;/', $record['txt'])
239
                && preg_match('/p=(none|quarantine|reject);/', $record['txt'])
240
            ) {
241
                // Required DMARC tags are present and valid.
242
                $message .= $checkmark . get_string('dmarctagsfound', 'local_mailtest')  . '<br>';
243
 
244
                // Check rua tag if present.
245
                $ruavalue = false;
246
                if (preg_match('/rua=([^;]+)/', $record['txt'], $matches)) {
247
                    $ruavalue = $matches[1];
248
                    // Validate rua value format (should be a valid URI).
249
                    if (!filter_var($ruavalue, FILTER_VALIDATE_URL) && !filter_var("mailto:" . $ruavalue, FILTER_VALIDATE_EMAIL)) {
250
                        // The rua value is not formatted correctly.
251
                        $message .= $xmark . get_string('dmarcruainvalid', 'local_mailtest')  . '<br>';
252
                        $success = false;
253
                    }
254
                }
255
 
256
                // Check ruf tag if present.
257
                $rufvalue = false;
258
                if (preg_match('/ruf=([^;]+)/', $record['txt'], $matches)) {
259
                    $rufvalue = $matches[1];
260
                    // Validate ruf value format (should be a valid URI).
261
                    if (!filter_var($rufvalue, FILTER_VALIDATE_URL) && !filter_var("mailto:" . $rufvalue, FILTER_VALIDATE_EMAIL)) {
262
                        // The ruf value is not formatted correctly.
263
                        $message .= $xmark . get_string('dmarcrufinvalid', 'local_mailtest')  . '<br>';
264
                        $success = false;
265
                    }
266
                }
267
 
268
                // Check pct tag if present.
269
                $pctvalue = false;
270
                if (preg_match('/pct=([0-9]+)/', $record['txt'], $matches)) {
271
                    $pctvalue = intval($matches[1]);
272
                    // Validate pct value range (should be between 0 and 100).
273
                    if ($pctvalue < 0 || $pctvalue > 100) {
274
                        // The pct value is not within the valid range.
275
                        $message .= $xmark . get_string('dmarcpctinvalid', 'local_mailtest')  . '<br>';
276
                        $success = false;
277
                    }
278
                }
279
                break;
280
            } else {
281
                // Required tags not found in any of the DMARC records.
282
                $message .= $checkmark . get_string('dmarctagsfound', 'local_mailtest')  . '<br>';
283
                $success = false;
284
            }
285
        }
286
    }
287
 
288
    // Check to ensure that either DKIM or SPF is configured.
289
    if (!$dkim && !$spf) {
290
        $message .= $xmark . get_string('dkimspffailed', 'local_mailtest')  . '<br>';
291
        $success = false;
292
    }
293
 
294
    // Check BIMI record.
295
 
296
    // Perform DNS query for BIMI TXT records.
297
    $bimirecords = @dns_get_record("_bimi." . $domain, DNS_TXT);
298
    if (empty($bimirecords)) {
299
        // Required tags not found in any of the DMARC records.
300
        $message .= $xmark . get_string('biminorecordfound', 'local_mailtest')  . '<br>';
301
        $success = false;
302
    } else {
303
        // Records found. Check if it has the required tags.
304
        $message .= $checkmark . get_string('bimirecordfound', 'local_mailtest')  . '<br>';
305
 
306
        // Loop through each BIMI record.
307
        foreach ($bimirecords as $record) {
308
            // Extract BIMI record data.
309
            $bimidata = $record['txt'];
310
 
311
            // Check if BIMI record contains both v and l tags.
312
            if (strpos($bimidata, 'v=BIMI1') !== false && strpos($bimidata, 'l=') !== false) {
313
                // Required DMARC tags are present and valid.
314
                $message .= $checkmark . get_string('bimitagsfound', 'local_mailtest')  . '<br>';
315
 
316
                // Extract logo URL from the BIMI record.
317
                preg_match('/l=([^;]+)/', $bimidata, $matches);
318
                $logourl = $matches[1];
319
 
320
                // Validate existence of logo URL.
321
                $headers = @get_headers($logourl);
322
                if ($headers && strpos($headers[0], '200')) {
323
                    // Logo URL exists, BIMI record is valid.
324
                    $message .= $checkmark . get_string('bimiinvalidlogo', 'local_mailtest', $logourl)  . '<br>';
325
                    break;
326
                }
327
            }
328
        }
329
        if (!$success) {
330
            // Required tags not found in any of the DMARC records.
331
            $message .= $xmark . get_string('bimidmarcfailure', 'local_mailtest')  . '<br>';
332
            $success = false;
333
        }
334
        if ($pctvalue != 100) {
335
            $message .= $xmark . get_string('bimipctinvalid', 'local_mailtest')  . '<br>';
336
            $success = false;
337
        }
338
    }
339
 
340
    $icon = $success ? 'fa-info-circle text-info' : 'fa-triangle-exclamation text-warning';
341
    $title = get_string('iconlabel', 'local_mailtest', $domain);
342
    $popupicon = '<a class="btn btn-link p-0" role="button" data-container="body" data-toggle="popover"'
343
        . ' data-placement="right" data-content="<div class=&quot;no-overflow&quot;><p>{message}</p></div>"'
344
        . ' data-html="true" tabindex="0" data-trigger="focus">'
345
        . '<i class="icon fa ' . $icon . ' fa-fw " title="' . $title . '" aria-label="' . $title . '"></i></a>';
346
    $message = str_replace('{message}', str_replace('"', '&quot;', $message), $popupicon);
347
 
348
    return $message;
349
}