Plugin Directory


Ignore:
Timestamp:
11/10/2026 11:35:38 PM (5 hours ago)
Author:
trainingbusinesspros
Message:

Update to version 1.5.2 from GitHub

File:
1 edited

Legend:

Unmodified
Added
Removed
  • mailhawk/trunk/includes/functions.php

    r3794810 r3795449  
    870870
    871871/**
     872 * A DNS record name as the two things a DNS provider may ask for: the host, which is the part before the sending
     873 * domain, and the full hostname. The MTA gives the DKIM name relative to the domain (postal-abc._domainkey) and the
     874 * return path as a full hostname (psrp.example.com), so both are turned into the same pair.
     875 *
     876 * @param string $name   the record name, relative to the domain or in full, @ for the domain itself
     877 * @param string $domain the sending domain
     878 *
     879 * @return array host and hostname, both lower case
     880 */
     881function get_dns_record_names( $name, $domain ) {
     882
     883        $name   = strtolower( trim( (string) $name, " \t\n\r\0\x0B." ) );
     884        $domain = strtolower( trim( (string) $domain, " \t\n\r\0\x0B." ) );
     885
     886        if ( '' === $name || '@' === $name || $name === $domain ) {
     887                return [ 'host' => '@', 'hostname' => $domain ];
     888        }
     889
     890        $suffix = '.' . $domain;
     891
     892        // Already the full hostname
     893        if ( '' !== $domain && substr( $name, - strlen( $suffix ) ) === $suffix ) {
     894                return [ 'host' => substr( $name, 0, - strlen( $suffix ) ), 'hostname' => $name ];
     895        }
     896
     897        return [ 'host' => $name, 'hostname' => '' !== $domain ? $name . $suffix : $name ];
     898}
     899
     900/**
     901 * Whether the domain may be a subdomain, so its records may have to go in the DNS of a parent domain. A domain of two
     902 * parts never is. One of three parts is not when the last two are a suffix people register domains under, like
     903 * co.uk. Anything else may be, it is better to say too much than to leave someone without the note they need.
     904 *
     905 * @param string $domain
     906 *
     907 * @return bool
     908 */
     909function is_maybe_subdomain( $domain ) {
     910
     911        $labels = explode( '.', strtolower( trim( (string) $domain, '. ' ) ) );
     912
     913        if ( count( $labels ) <= 2 ) {
     914                return false;
     915        }
     916
     917        /**
     918         * Suffixes of two parts that domains are registered under, like co.uk, so example.co.uk is not taken for a subdomain
     919         *
     920         * @param string[] $suffixes
     921         */
     922        $suffixes = apply_filters( 'mailhawk/second_level_suffixes', [
     923                'co.uk', 'org.uk', 'me.uk', 'ltd.uk', 'plc.uk', 'net.uk', 'ac.uk', 'gov.uk', 'sch.uk',
     924                'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'id.au', 'asn.au',
     925                'co.nz', 'net.nz', 'org.nz', 'ac.nz', 'govt.nz',
     926                'co.za', 'org.za', 'net.za',
     927                'co.in', 'net.in', 'org.in', 'firm.in', 'gen.in', 'ind.in',
     928                'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp',
     929                'co.kr', 'or.kr', 'co.il', 'org.il', 'co.id', 'or.id', 'co.th', 'in.th',
     930                'com.br', 'net.br', 'org.br', 'com.mx', 'org.mx', 'com.ar', 'com.co', 'com.pe', 'com.ve', 'com.ec', 'com.uy',
     931                'com.cn', 'net.cn', 'org.cn', 'com.hk', 'org.hk', 'com.tw', 'org.tw', 'com.sg', 'org.sg', 'com.my', 'org.my',
     932                'com.ph', 'com.vn', 'com.pk', 'com.bd', 'com.tr', 'org.tr', 'com.sa', 'com.eg', 'com.ng', 'com.gh', 'co.ke',
     933                'com.ua', 'org.ua', 'com.pl', 'org.pl', 'com.es', 'org.es', 'com.pt', 'com.gr', 'co.at', 'or.at', 'com.cy',
     934        ] );
     935
     936        return ! ( 3 === count( $labels ) && in_array( $labels[1] . '.' . $labels[2], (array) $suffixes, true ) );
     937}
     938
     939/**
     940 * Plain text instructions for adding a domain's records to its DNS, for whoever does it: an agency, the site's
     941 * developer or an AI agent. Everything needed is in the text, nobody has to see the DNS screen.
     942 *
     943 * @param object $domain a domain from the MTA
     944 *
     945 * @return string
     946 */
     947function get_dns_instructions( $domain ) {
     948
     949        $name    = strtolower( (string) $domain->name );
     950        $dns     = \MailHawk\Abilities\Formatter::dns_records( $domain );
     951        $records = $dns['records'];
     952
     953        $labels = [
     954                'dkim'        => __( 'DKIM, which signs the email so receivers can tell it really came from this domain', 'mailhawk' ),
     955                'return_path' => __( 'Return path, where bounces go', 'mailhawk' ),
     956                'spf'         => __( 'SPF, which says MailHawk may send email for this domain', 'mailhawk' ),
     957        ];
     958
     959        $to_add = array_filter( $records, function ( $record ) {
     960                return ! $record['verified'];
     961        } );
     962
     963        $lines = [];
     964
     965        /* translators: %s: the sending domain */
     966        $lines[] = sprintf( __( 'DNS records for %s', 'mailhawk' ), $name );
     967        $lines[] = '';
     968        /* translators: %s: the sending domain */
     969        $lines[] = sprintf( __( 'The WordPress site sends its email through MailHawk (mailhawk.io). Add the records below at the DNS provider of %s so that email is authenticated with SPF, DKIM and a return path, and reaches the inbox.', 'mailhawk' ), $name );
     970
     971        if ( empty( $to_add ) ) {
     972                $lines[] = __( 'All of them are already in place and verified, nothing needs to change.', 'mailhawk' );
     973        } else if ( count( $to_add ) < count( $records ) ) {
     974                $lines[] = __( 'Records marked "already verified" are in place already and can be left alone.', 'mailhawk' );
     975        }
     976
     977        $lines[] = '';
     978        $lines[] = __( 'How to enter the names:', 'mailhawk' );
     979        /* translators: %s: the sending domain */
     980        $lines[] = '- ' . sprintf( __( 'Each record has a Host, the part of the name before %s, and a Full hostname. Most DNS providers ask for the Host in a field called Host or Name. Use the Full hostname only if the provider asks for the whole name.', 'mailhawk' ), $name );
     981        $lines[] = '- ' . __( 'If the provider adds the domain to the name by itself, enter the Host, or the domain ends up in the name twice.', 'mailhawk' );
     982        if ( is_maybe_subdomain( $name ) ) {
     983                /* translators: %s: the sending domain */
     984                $lines[] = '- ' . sprintf( __( 'If %s is a subdomain and its records are kept in the DNS of a parent domain, the Host is the Full hostname without that parent domain. For example, for academy.example.com in the DNS of example.com, postal-abc._domainkey becomes postal-abc._domainkey.academy.', 'mailhawk' ), $name );
     985        }
     986
     987        $lines[] = '- ' . __( 'An @ as the Host means the domain itself. Some providers want the field left empty for that.', 'mailhawk' );
     988
     989        foreach ( $records as $i => $record ) {
     990
     991                $lines[] = '';
     992                $lines[] = sprintf( '%d. %s%s', $i + 1, $labels[ $record['purpose'] ] ?? $record['purpose'], $record['verified'] ? ' ' . __( '(already verified)', 'mailhawk' ) : '' );
     993                /* translators: %s: DNS record type, like TXT or CNAME */
     994                $lines[] = '   ' . sprintf( __( 'Type: %s', 'mailhawk' ), $record['type'] );
     995                /* translators: %s: the host of the DNS record */
     996                $lines[] = '   ' . sprintf( __( 'Host: %s', 'mailhawk' ), $record['host'] );
     997                /* translators: %s: the full hostname of the DNS record */
     998                $lines[] = '   ' . sprintf( __( 'Full hostname: %s', 'mailhawk' ), $record['hostname'] );
     999                /* translators: %s: the value of the DNS record */
     1000                $lines[] = '   ' . sprintf( __( 'Value: %s', 'mailhawk' ), $record['value'] );
     1001                /* translators: %d: time to live of the DNS record, in seconds */
     1002                $lines[] = '   ' . sprintf( __( 'TTL: %d (or the provider\'s default)', 'mailhawk' ), $record['ttl'] );
     1003
     1004                if ( 'return_path' === $record['purpose'] ) {
     1005                        $lines[] = '   ' . __( 'A CNAME can not share its name with any other record. If the provider proxies DNS records (like Cloudflare), set this one to DNS only.', 'mailhawk' );
     1006                }
     1007
     1008                if ( 'spf' === $record['purpose'] && ! $record['verified'] ) {
     1009
     1010                        if ( $dns['existing_spf_record'] ) {
     1011                                /* translators: %s: the SPF record the domain has now */
     1012                                $lines[] = '   ' . sprintf( __( 'The domain already has an SPF record: %s. A name can only have one, so do not add a second TXT record starting with v=spf1. Replace the existing one with the value above, which is the same record with MailHawk added.', 'mailhawk' ), $dns['existing_spf_record'] );
     1013                        } else {
     1014                                $lines[] = '   ' . __( 'A name can only have one SPF record. If a TXT record starting with v=spf1 is there already, add the include: part of the value above to it, before the last part (like ~all), instead of adding a second one.', 'mailhawk' );
     1015                        }
     1016                }
     1017        }
     1018
     1019        $lines[] = '';
     1020        $lines[] = __( 'When the records are added:', 'mailhawk' );
     1021        $lines[] = '- ' . __( 'DNS changes usually show up within minutes, but can take up to 24 hours.', 'mailhawk' );
     1022        /* translators: %s: URL of the DNS screen of this domain in wp-admin */
     1023        $lines[] = '- ' . sprintf( __( 'Then click Verify on the MailHawk DNS screen in WordPress: %s', 'mailhawk' ), mailhawk_admin_page( [ 'view' => 'domains', 'domain' => $name ] ) );
     1024
     1025        return implode( "\n", $lines );
     1026}
     1027
     1028/**
    8721029 * Get the value of the SPF record or false if it doesn't exist.
    8731030 *
    … …  
    10501207function get_default_from_email_address() {
    10511208        return get_option( 'mailhawk_default_from_email_address' );
     1209}
     1210
     1211/**
     1212 * The addresses to suggest sending a test email from: the default from address first, then the same name at each of
     1213 * the domains registered with MailHawk, and the current user's and the site's own address when their domain is one of
     1214 * them. Each says whether its domain is authenticated, so a test from it can be expected to reach the inbox.
     1215 *
     1216 * @param object[]|mixed $domains the domains from the MTA, anything else is treated as none
     1217 *
     1218 * @return array{addresses: array[], domains: bool[]} addresses with email, default and authenticated (null when the
     1219 *                                                    domain is not registered), and each registered domain with whether
     1220 *                                                    it is authenticated
     1221 */
     1222function get_test_from_addresses( $domains ) {
     1223
     1224        $authenticated = [];
     1225
     1226        foreach ( is_array( $domains ) ? $domains : [] as $domain ) {
     1227                if ( is_object( $domain ) && ! empty( $domain->name ) ) {
     1228                        $authenticated[ strtolower( (string) $domain->name ) ] = is_domain_authenticated( $domain );
     1229                }
     1230        }
     1231
     1232        // Authenticated domains first, then by name
     1233        $flags = $authenticated;
     1234
     1235        uksort( $authenticated, function ( $a, $b ) use ( $flags ) {
     1236                return [ ! $flags[ $a ], $a ] <=> [ ! $flags[ $b ], $b ];
     1237        } );
     1238
     1239        $default = strtolower( (string) get_default_from_email_address() );
     1240        $local   = is_email( $default ) ? strstr( $default, '@', true ) : 'wordpress';
     1241
     1242        $emails = [ $default ];
     1243
     1244        foreach ( array_keys( $authenticated ) as $name ) {
     1245                $emails[] = $local . '@' . $name;
     1246        }
     1247
     1248        // People's own addresses, only on a domain MailHawk can send for
     1249        foreach ( [ wp_get_current_user()->user_email, get_option( 'admin_email' ) ] as $email ) {
     1250                $email = strtolower( (string) $email );
     1251
     1252                if ( is_email( $email ) && isset( $authenticated[ substr( strrchr( $email, '@' ), 1 ) ] ) ) {
     1253                        $emails[] = $email;
     1254                }
     1255        }
     1256
     1257        $addresses = [];
     1258
     1259        foreach ( array_unique( array_filter( $emails, 'is_email' ) ) as $email ) {
     1260
     1261                $domain = substr( strrchr( $email, '@' ), 1 );
     1262
     1263                $addresses[] = [
     1264                        'email'         => $email,
     1265                        'default'       => $email === $default,
     1266                        'authenticated' => $authenticated[ $domain ] ?? null,
     1267                ];
     1268        }
     1269
     1270        return [ 'addresses' => $addresses, 'domains' => $authenticated ];
     1271}
     1272
     1273/**
     1274 * Where a test email can end up and what to make of it, for the test email itself and for the Test screen. Answers are
     1275 * HTML with only links and emphasis.
     1276 *
     1277 * @param bool $not_arrived whether to include what to do when it never arrived, which only makes sense on the screen
     1278 *
     1279 * @return array[] each with key, question and answer
     1280 */
     1281function get_test_email_faq( $not_arrived = false ) {
     1282
     1283        $domains = mailhawk_admin_page( [ 'view' => 'domains' ] );
     1284        $log     = mailhawk_admin_page( [ 'view' => 'log' ] );
     1285
     1286        $faq = [
     1287                [
     1288                        'key'      => 'inbox',
     1289                        'question' => __( 'It landed in the inbox', 'mailhawk' ),
     1290                        'answer'   => __( 'MailHawk is working, and the mailbox trusts email from this site. There is nothing else to do.', 'mailhawk' ),
     1291                ],
     1292                [
     1293                        'key'      => 'updates',
     1294                        'question' => __( 'Gmail put it in Updates', 'mailhawk' ),
     1295                        'answer'   => __( 'That is normal. Gmail puts receipts, password resets and other notifications in Updates, and they still arrive. To keep email from this address in Primary, drag it to the Primary tab and choose to do the same for future messages.', 'mailhawk' ),
     1296                ],
     1297                [
     1298                        'key'      => 'promotions',
     1299                        'question' => __( 'Gmail put it in Promotions', 'mailhawk' ),
     1300                        'answer'   => __( 'That is normal too, and not a delivery problem. Gmail sorts email into tabs by what it looks like, and branded emails with images often go to Promotions. Your newsletters probably will as well. Order and account emails that are plain and personal usually end up in Primary or Updates. You can drag it to Primary to teach your own inbox.', 'mailhawk' ),
     1301                ],
     1302                [
     1303                        'key'      => 'other',
     1304                        'question' => __( 'Outlook put it in Other', 'mailhawk' ),
     1305                        'answer'   => __( 'That is normal. Outlook\'s Focused Inbox puts email from senders it has not seen before in Other. Move it to Focused and choose to always move email from this address there.', 'mailhawk' ),
     1306                ],
     1307                [
     1308                        'key'      => 'spam',
     1309                        'question' => __( 'It landed in spam', 'mailhawk' ),
     1310                        'answer'   => sprintf(
     1311                        /* translators: %s: URL of the Domains screen in wp-admin */
     1312                                __( 'Check that the domain of the address it came from has all its DNS records verified on the <a href="%s">Domains screen</a>. An address on a domain that is not verified, or on a free provider like gmail.com, can not be authenticated and is often treated as spam. Then mark the email as not spam, which teaches your mailbox to trust it. A new domain can take a few days of sending to build trust. If it keeps happening, let MailHawk support know.', 'mailhawk' ),
     1313                                esc_url( $domains )
     1314                        ),
     1315                ],
     1316        ];
     1317
     1318        if ( $not_arrived ) {
     1319                $faq[] = [
     1320                        'key'      => 'missing',
     1321                        'question' => __( 'It did not arrive at all', 'mailhawk' ),
     1322                        'answer'   => sprintf(
     1323                        /* translators: %s: URL of the Email Log screen in wp-admin */
     1324                                __( 'Give it a minute, then look in spam and in every tab or folder. The <a href="%s">Email Log</a> says what happened to it: failed emails say why, and an email held for review can be released from there. Also check the address it was sent to for typos.', 'mailhawk' ),
     1325                                esc_url( $log )
     1326                        ),
     1327                ];
     1328        }
     1329
     1330        return $faq;
     1331}
     1332
     1333/**
     1334 * The test email: its subject, a branded HTML version and the same in plain text. Its text is fixed, the test is to see
     1335 * that email arrives and where, and an email anyone could put any text in would be a way to send spam.
     1336 *
     1337 * @param string $to   who it is sent to
     1338 * @param string $from the address it is sent from, the default from address when empty
     1339 *
     1340 * @return array{subject: string, html: string, text: string}
     1341 */
     1342function get_test_email( $to, $from = '' ) {
     1343
     1344        $site    = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
     1345        $from    = $from ?: (string) get_default_from_email_address();
     1346        $sent_at = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );
     1347        $faq     = get_test_email_faq();
     1348
     1349        /* translators: %s: the name of the site */
     1350        $subject = sprintf( __( 'MailHawk test email from %s', 'mailhawk' ), $site );
     1351
     1352        ob_start();
     1353        include dirname( __DIR__ ) . '/templates/emails/test-email.php';
     1354        $html = ob_get_clean();
     1355
     1356        $text = [
     1357                __( 'It works!', 'mailhawk' ),
     1358                '',
     1359                /* translators: 1: the name of the site, 2: the address it was sent from, 3: the address it was sent to, 4: date and time it was sent */
     1360                sprintf( __( 'This test email from %1$s went out through MailHawk, from %2$s to %3$s on %4$s.', 'mailhawk' ), $site, $from, $to, $sent_at ),
     1361                '',
     1362                __( 'Where did it land?', 'mailhawk' ),
     1363        ];
     1364
     1365        foreach ( $faq as $item ) {
     1366                $text[] = '';
     1367                $text[] = $item['question'];
     1368                // Links keep their address in plain text
     1369                $text[] = html_entity_decode( wp_strip_all_tags( preg_replace( '/<a [^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/', '$2 ($1)', $item['answer'] ) ), ENT_QUOTES, 'UTF-8' );
     1370        }
     1371
     1372        $text[] = '';
     1373        /* translators: %s: the address of the site */
     1374        $text[] = sprintf( __( 'Sent by the MailHawk plugin on %s. You can ignore this email.', 'mailhawk' ), home_url_no_scheme() );
     1375
     1376        return [ 'subject' => $subject, 'html' => $html, 'text' => implode( "\n", $text ) ];
     1377}
     1378
     1379/**
     1380 * Give the next email that is sent this plain text version, for an HTML email whose own text would read badly
     1381 *
     1382 * @param string $text
     1383 *
     1384 * @return \Closure the hook, to remove when the email failed before it got to it, so the next email does not get it
     1385 */
     1386function set_next_alt_body( $text ) {
     1387
     1388        $set = function ( $phpmailer ) use ( $text, &$set ) {
     1389                $phpmailer->AltBody = $text;
     1390                remove_action( 'phpmailer_init', $set, 99 );
     1391        };
     1392
     1393        add_action( 'phpmailer_init', $set, 99 );
     1394
     1395        return $set;
    10521396}
    10531397
    … …  
    13931737        $risk = 0;
    13941738
    1395         // Test quarantine system using + synatx
     1739        // Test quarantine system using + syntax
    13961740        if ( str_contains( $email_address, '+test-quarantine@' ) ) {
    13971741                return 999;
Note: See TracChangeset for help on using the changeset viewer.