Changeset 3795449 for mailhawk/trunk/includes/functions.php
- Timestamp:
- 11/10/2026 11:35:38 PM (5 hours ago)
- File:
-
- 1 edited
-
mailhawk/trunk/includes/functions.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mailhawk/trunk/includes/functions.php
r3794810 r3795449 870 870 871 871 /** 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 */ 881 function 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 */ 909 function 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 */ 947 function 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 /** 872 1029 * Get the value of the SPF record or false if it doesn't exist. 873 1030 * … … 1050 1207 function get_default_from_email_address() { 1051 1208 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 */ 1222 function 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 */ 1281 function 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 */ 1342 function 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 */ 1386 function 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; 1052 1396 } 1053 1397 … … 1393 1737 $risk = 0; 1394 1738 1395 // Test quarantine system using + syn atx1739 // Test quarantine system using + syntax 1396 1740 if ( str_contains( $email_address, '+test-quarantine@' ) ) { 1397 1741 return 999;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)