Changeset 3795449
- Timestamp:
- 11/10/2026 11:35:38 PM (5 hours ago)
- Location:
- mailhawk
- Files:
-
- 2 added
- 40 edited
- 1 copied
-
tags/1.5.2 (copied) (copied from mailhawk/trunk )
-
tags/1.5.2/admin/admin.php (modified) (6 diffs)
-
tags/1.5.2/admin/views/dns-single.php (modified) (7 diffs)
-
tags/1.5.2/admin/views/dns.php (modified) (2 diffs)
-
tags/1.5.2/admin/views/overview.php (modified) (3 diffs)
-
tags/1.5.2/admin/views/suspended.php (modified) (1 diff)
-
tags/1.5.2/admin/views/test.php (modified) (1 diff)
-
tags/1.5.2/assets/css/admin.css (modified) (2 diffs)
-
tags/1.5.2/assets/js/admin.js (modified) (2 diffs)
-
tags/1.5.2/includes/abilities/ability.php (modified) (1 diff)
-
tags/1.5.2/includes/abilities/email/send-test-email.php (modified) (4 diffs)
-
tags/1.5.2/includes/abilities/formatter.php (modified) (8 diffs)
-
tags/1.5.2/includes/abilities/status/get-status.php (modified) (2 diffs)
-
tags/1.5.2/includes/api/postal/messages.php (modified) (1 diff)
-
tags/1.5.2/includes/api/postal/postal.php (modified) (4 diffs)
-
tags/1.5.2/includes/functions.php (modified) (3 diffs)
-
tags/1.5.2/includes/quarantine.php (modified) (1 diff)
-
tags/1.5.2/includes/site-health.php (modified) (6 diffs)
-
tags/1.5.2/mailhawk.php (modified) (2 diffs)
-
tags/1.5.2/readme.txt (modified) (2 diffs)
-
tags/1.5.2/templates/emails/test-email.php (added)
-
tags/1.5.2/uninstall.php (modified) (2 diffs)
-
trunk/admin/admin.php (modified) (6 diffs)
-
trunk/admin/views/dns-single.php (modified) (7 diffs)
-
trunk/admin/views/dns.php (modified) (2 diffs)
-
trunk/admin/views/overview.php (modified) (3 diffs)
-
trunk/admin/views/suspended.php (modified) (1 diff)
-
trunk/admin/views/test.php (modified) (1 diff)
-
trunk/assets/css/admin.css (modified) (2 diffs)
-
trunk/assets/js/admin.js (modified) (2 diffs)
-
trunk/includes/abilities/ability.php (modified) (1 diff)
-
trunk/includes/abilities/email/send-test-email.php (modified) (4 diffs)
-
trunk/includes/abilities/formatter.php (modified) (8 diffs)
-
trunk/includes/abilities/status/get-status.php (modified) (2 diffs)
-
trunk/includes/api/postal/messages.php (modified) (1 diff)
-
trunk/includes/api/postal/postal.php (modified) (4 diffs)
-
trunk/includes/functions.php (modified) (3 diffs)
-
trunk/includes/quarantine.php (modified) (1 diff)
-
trunk/includes/site-health.php (modified) (6 diffs)
-
trunk/mailhawk.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/emails/test-email.php (added)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mailhawk/tags/1.5.2/admin/admin.php
r3794810 r3795449 15 15 use function MailHawk\dismiss_notice; 16 16 use function MailHawk\get_array_var; 17 use function MailHawk\get_test_email; 18 use function MailHawk\set_next_alt_body; 17 19 use function MailHawk\get_dismissible_notices; 18 20 use function MailHawk\get_hostname; … … 579 581 } 580 582 581 $to = sanitize_email( get_post_var( 'to_address' ) ); 582 $subject = sanitize_text_field( get_post_var( 'subject' ) ); 583 $body = sanitize_textarea_field( get_post_var( 'content' ) ); 584 585 $this->test_data['to'] = $to; 586 $this->test_data['subject'] = $subject; 587 $this->test_data['body'] = $body; 583 $to = sanitize_email( get_post_var( 'to_address' ) ); 584 $from = sanitize_email( get_post_var( 'from_address' ) ); 585 586 $this->test_data['to'] = $to; 587 $this->test_data['from'] = $from; 588 588 589 589 if ( ! $to ) { … … 593 593 add_action( 'wp_mail_failed', [ $this, 'maybe_email_failed' ] ); 594 594 595 $success = wp_mail( $to, $subject, $body, [ 596 'sender' => 'app@mailhawk.io' 597 ] ); 595 // Always the same email, what is being tested is whether it arrives and where 596 $email = get_test_email( $to, $from ); 597 $headers = [ 'Content-Type: text/html' ]; 598 599 // From the address that was picked, with the default from name. Without one it is the default from address. 600 if ( $from ) { 601 $name = (string) get_option( 'mailhawk_default_from_name' ); 602 $headers[] = $name ? sprintf( 'From: %s <%s>', $name, $from ) : sprintf( 'From: %s', $from ); 603 } 604 605 $alt_body = set_next_alt_body( $email['text'] ); 606 607 $success = wp_mail( $to, $email['subject'], $email['html'], $headers ); 608 609 remove_action( 'phpmailer_init', $alt_body, 99 ); 598 610 599 611 if ( $success ) { … … 1100 1112 wp_localize_script( 'mailhawk-admin', 'MailHawkConnect', [ 1101 1113 'connecting_text' => '<span class="dashicons dashicons-admin-generic"></span>' . __( 'Connecting You To MailHawk...', 'mailhawk' ) 1114 ] ); 1115 1116 // What the DNS screen says when a name is copied 1117 wp_localize_script( 'mailhawk-admin', 'MailHawkDns', [ 1118 'host_copied' => __( 'Host copied', 'mailhawk' ), 1119 'hostname_copied' => __( 'Full hostname copied', 'mailhawk' ), 1102 1120 ] ); 1103 1121 … … 1403 1421 <p> 1404 1422 <?php if ( $suspension['reason'] ) : ?> 1405 <?php printf( __( '<b>Attention:</b> MailHawk has suspended your account, so email from this site is not being sent. The reason given: %s', 'mailhawk' ), esc_html( $suspension['reason'] ) ); ?> 1423 <?php 1424 /* translators: %s: the reason MailHawk gave for suspending the account */ 1425 printf( __( '<b>Attention:</b> MailHawk has suspended your account, so email from this site is not being sent. Reason: %s', 'mailhawk' ), esc_html( $suspension['reason'] ) ); 1426 ?> 1406 1427 <?php else : ?> 1407 1428 <?php _e( '<b>Attention:</b> It looks like your account has been suspended. To continue sending email you must reactivate your account.', 'mailhawk' ); ?> 1408 1429 <?php endif; ?> 1409 <?php if ( $since ) : ?>1410 <?php printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); ?>1411 <?php endif; ?>1412 <?php if ( 'organization' === $suspension['scope'] ) : ?>1413 <?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?>1414 <?php endif; ?>1415 1430 </p> 1431 <?php if ( $since || 'organization' === $suspension['scope'] ) : ?> 1432 <p> 1433 <?php if ( $since ) : ?> 1434 <?php 1435 /* translators: %s: date and time the account was suspended */ 1436 printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); 1437 ?> 1438 <?php endif; ?> 1439 <?php if ( 'organization' === $suspension['scope'] ) : ?> 1440 <?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?> 1441 <?php endif; ?> 1442 </p> 1443 <?php endif; ?> 1416 1444 <p> 1417 1445 <a href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener" … … 1549 1577 ?> 1550 1578 <div class="notice notice-success is-dismissible"> 1551 <p><?php printf( __( 'We sent a test email to %s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>" ); ?></p> 1579 <p><?php 1580 if ( get_array_var( $this->test_data, 'from' ) ) { 1581 /* translators: 1: recipient of the test email, 2: the address it was sent from */ 1582 printf( __( 'We sent a test email to %1$s from %2$s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>", "<code>" . esc_html( get_array_var( $this->test_data, 'from' ) ) . "</code>" ); 1583 } else { 1584 /* translators: %s: recipient of the test email */ 1585 printf( __( 'We sent a test email to %s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>" ); 1586 } 1587 ?></p> 1552 1588 </div> 1553 1589 <?php -
mailhawk/tags/1.5.2/admin/views/dns-single.php
r3788980 r3795449 4 4 use MailHawk\Api\Postal\Domains; 5 5 use function MailHawk\mailhawk_admin_page; 6 use function MailHawk\get_dns_instructions; 7 use function MailHawk\get_dns_record_names; 8 use function MailHawk\is_maybe_subdomain; 6 9 use function MailHawk\get_spf_record; 7 10 use function MailHawk\get_url_var; … … 47 50 <?php endif; ?> 48 51 52 <?php 53 54 // The same pair for every record: what goes in the Host or Name field, and the hostname it ends up as 55 $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name, $domain->name ); 56 $return_path_names = get_dns_record_names( $domain->return_path->return_path_domain, $domain->name ); 57 $spf_names = get_dns_record_names( '@', $domain->name ); 58 59 /** 60 * The full hostname in one pill, with the host inside it as a pill of its own. The host copies just the host, the 61 * rest of it copies the full hostname. 62 * 63 * @param array $names 64 */ 65 $name_fields = function ( $names ) use ( $domain ) { 66 67 // The domain itself has no part before the domain, so it is shown after the @ 68 $rest = '@' === $names['host'] ? strtolower( $domain->name ) : substr( $names['hostname'], strlen( $names['host'] ) ); 69 70 ?> 71 <div class="mailhawk-dns-name code" role="button" tabindex="0" 72 data-copy="<?php echo esc_attr( $names['hostname'] ); ?>" 73 title="<?php esc_attr_e( 'Copy the full hostname', 'mailhawk' ); ?>" 74 aria-label="<?php echo esc_attr( sprintf( 75 /* translators: %s: the full hostname of a DNS record */ 76 __( 'Copy the full hostname %s', 'mailhawk' ), $names['hostname'] ) ); ?>"> 77 <button type="button" class="mailhawk-dns-host" 78 data-copy="<?php echo esc_attr( $names['host'] ); ?>" 79 title="<?php esc_attr_e( 'Copy the host', 'mailhawk' ); ?>" 80 aria-label="<?php echo esc_attr( sprintf( 81 /* translators: %s: the host of a DNS record, relative to the domain */ 82 __( 'Copy the host %s', 'mailhawk' ), $names['host'] ) ); ?>"><?php echo esc_html( $names['host'] ); ?></button> 83 <span class="mailhawk-dns-rest<?php echo '@' === $names['host'] ? ' is-domain' : ''; ?>"><?php echo esc_html( $rest ); ?></span> 84 <span class="mailhawk-dns-copied" aria-live="polite"></span> 85 <span class="dashicons dashicons-admin-page" aria-hidden="true"></span> 86 </div> 87 <?php 88 }; 89 90 /** 91 * A pill like the name, without a part of its own: the whole of it copies the value 92 * 93 * @param string $value 94 * @param string $label what the value is, for a screen reader 95 */ 96 $copy_field = function ( $value, $label ) { 97 ?> 98 <div class="mailhawk-dns-name mailhawk-copy-value code" role="button" tabindex="0" 99 data-copy="<?php echo esc_attr( $value ); ?>" 100 data-copied="<?php esc_attr_e( 'Copied', 'mailhawk' ); ?>" 101 title="<?php echo esc_attr( $value ); ?>" 102 aria-label="<?php echo esc_attr( sprintf( 103 /* translators: 1: what is copied, like TTL or value, 2: the text that is copied */ 104 __( 'Copy the %1$s %2$s', 'mailhawk' ), $label, $value ) ); ?>"> 105 <span class="mailhawk-copy-text"><?php echo esc_html( $value ); ?></span> 106 <span class="mailhawk-dns-copied" aria-live="polite"></span> 107 <span class="dashicons dashicons-admin-page" aria-hidden="true"></span> 108 </div> 109 <?php 110 }; 111 112 $ttl_label = __( 'TTL', 'mailhawk' ); 113 $value_label = __( 'value', 'mailhawk' ); 114 115 ?> 116 <p class="description"> 117 <?php _e( 'Click the highlighted <b>host</b> to copy just that part, which is what most DNS providers ask for, or anywhere else on the name to copy the <b>full hostname</b>.', 'mailhawk' ); ?> 118 <?php if ( is_maybe_subdomain( $domain->name ) ) : ?> 119 <?php _e( 'If the records of a subdomain go in the DNS of its parent domain, add the subdomain to the host: for <code>academy.example.com</code> in the DNS of <code>example.com</code>, <code>postal-abc._domainkey</code> becomes <code>postal-abc._domainkey.academy</code>.', 'mailhawk' ); ?> 120 <?php endif; ?> 121 </p> 122 49 123 <table class="wp-list-table widefat fixed striped dns-list"> 50 124 <thead> … … 60 134 <tr> 61 135 <td><?php _e( '<code>TXT</code> (DKIM)' ) ?></td> 62 <td><input class="code" onfocus="this.select()" type="text" 63 value="<?php esc_attr_e( strtolower( $domain->dkim->dkim_record_name ) ); ?>" 64 readonly></td> 65 <td><input class="code" onfocus="this.select()" type="text" 66 value="3600" 67 readonly></td> 68 <td><input class="code" onfocus="this.select()" type="text" 69 value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>" 70 readonly></td> 136 <td><?php $name_fields( $dkim_names ); ?></td> 137 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 138 <td><?php $copy_field( (string) $domain->dkim->dkim_record, $value_label ); ?></td> 71 139 <td><?php echo $domain->dkim->dkim_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 72 140 </td> … … 74 142 <tr> 75 143 <td><?php _e( '<code>CNAME</code>' ) ?></td> 76 <td><input class="code" onfocus="this.select()" type="text" 77 value="<?php esc_attr_e( strtolower( $domain->return_path->return_path_domain ) ); ?>" 78 readonly></td> 79 <td><input class="code" onfocus="this.select()" type="text" 80 value="3600" 81 readonly></td> 82 <td><input class="code" onfocus="this.select()" type="text" 83 value="rp.mta01.mailhawk.io" 84 readonly></td> 144 <td><?php $name_fields( $return_path_names ); ?></td> 145 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 146 <td><?php $copy_field( 'rp.mta01.mailhawk.io', $value_label ); ?></td> 85 147 <td><?php echo $domain->return_path->return_path_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 86 148 </td> … … 88 150 <tr> 89 151 <td><?php _e( '<code>TXT</code> (SPF)' ) ?></td> 90 <td><input class="code" onfocus="this.select()" type="text" 91 value="@" 92 readonly></td> 93 <td><input class="code" onfocus="this.select()" type="text" 94 value="3600" 95 readonly></td> 152 <td><?php $name_fields( $spf_names ); ?></td> 153 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 96 154 <td> 97 155 … … 109 167 endif; ?> 110 168 111 <input class="code" onfocus="this.select()" type="text" 112 value="<?php esc_attr_e( $spf_record ); ?>" 113 readonly></td> 169 <?php $copy_field( (string) $spf_record, $value_label ); ?></td> 114 170 <td><?php echo $domain->spf->spf_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 115 171 </td> … … 122 178 <a href="https://mailhawk.io/configure-dns/" 123 179 target="_blank"><?php _e( 'Learn how to configure your DNS', 'mailhawk' ); ?></a></p> 180 <p class="mailhawk-dns-handoff"> 181 <?php _e( 'Someone else manages your DNS, like an agency, a developer or an AI assistant?', 'mailhawk' ); ?> 182 <button type="button" class="button button-secondary mailhawk-copy-button" 183 data-copy="<?php echo esc_attr( get_dns_instructions( $domain ) ); ?>" 184 data-copied="<?php esc_attr_e( 'Instructions copied', 'mailhawk' ); ?>"> 185 <span class="dashicons dashicons-clipboard" aria-hidden="true"></span> 186 <span class="mailhawk-copy-button-label"><?php _e( 'Copy instructions', 'mailhawk' ); ?></span> 187 </button> 188 </p> 124 189 </div> 125 190 <p> -
mailhawk/tags/1.5.2/admin/views/dns.php
r3788980 r3795449 3 3 // Todo get domains from API 4 4 use MailHawk\Api\Postal\Domains; 5 use function MailHawk\get_dns_record_names; 6 use function MailHawk\is_maybe_subdomain; 5 7 use function MailHawk\mailhawk_admin_page; 6 8 … … 58 60 readonly> 59 61 <h3><?php _e( 'DKIM Record' ); ?></h3> 60 <p class="description"><?php printf( __( 'You need to add a new TXT record with the name <code>%s</code> with the following content.', 'mailhawk' ), esc_html( strtolower( $domain->dkim->dkim_record_name ) ) ); ?></p> 62 <?php $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name, $domain->name ); ?> 63 <p class="description"><?php printf( 64 /* translators: 1: the record host, relative to the domain, 2: the full hostname */ 65 __( 'You need to add a new TXT record with the host <code>%1$s</code> (full hostname <code>%2$s</code>) with the following content.', 'mailhawk' ), 66 esc_html( $dkim_names['host'] ), 67 esc_html( $dkim_names['hostname'] ) 68 ); ?> 69 <?php if ( is_maybe_subdomain( $domain->name ) ) : ?> 70 <?php _e( 'If you add it in the DNS of a parent domain, add the subdomain to the host.', 'mailhawk' ); ?> 71 <?php endif; ?></p> 61 72 <input class="code" onfocus="this.select()" type="text" 62 73 value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>" -
mailhawk/tags/1.5.2/admin/views/overview.php
r3794810 r3795449 148 148 <?php if ( $is_throttled ): ?> 149 149 <div class="mailhawk-content-box mailhawk-throttle-notice"> 150 <h2><?php _e( ' Email is being held back', 'mailhawk' ); ?></h2>150 <h2><?php _e( 'Your monthly email limit is reached', 'mailhawk' ); ?></h2> 151 151 <p> 152 152 <?php _e( "This site has used all of its emails for the month, so MailHawk is turning email away instead of sending it. The emails it turns away are marked as failed in the log.", 'mailhawk' ); ?> 153 153 <?php if ( $resets_at ): ?> 154 <?php printf( __( 'Sending starts again when the limit resets on %s (UTC), or as soon as the plan is upgraded.', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?> 154 <?php 155 /* translators: %s: date the monthly limit resets */ 156 printf( __( 'Sending starts again when the limit resets on %s (UTC), or as soon as the plan is upgraded.', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); 157 ?> 155 158 <?php else: ?> 156 159 <?php _e( 'Sending starts again when the limit resets, or as soon as the plan is upgraded.', 'mailhawk' ); ?> … … 234 237 </div> 235 238 <p> 236 <?php printf( 'Used: <b>%s</b> / %s', number_format_i18n( $usage ), number_format_i18n( $limit ) ); ?> 239 <?php 240 /* translators: 1: emails used this month, 2: monthly limit */ 241 printf( __( 'Used: %1$s / %2$s', 'mailhawk' ), '<b>' . number_format_i18n( $usage ) . '</b>', number_format_i18n( $limit ) ); 242 ?> 237 243 <?php if ( $resets_at ): ?> 238 <span class="usage-resets">· <?php printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?></span> 244 <span class="usage-resets">· <?php 245 /* translators: %s: date the monthly limit resets */ 246 printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); 247 ?></span> 239 248 <?php endif; ?> 240 249 </p> … … 246 255 <?php _e( 'Upgrade your plan so your email keeps going out.', 'mailhawk' ); ?> 247 256 <?php else: ?> 248 <b><?php printf( __( "You've used %s%% of this month's emails.", 'mailhawk' ), number_format_i18n( $percent_usage ) ); ?></b> 257 <b><?php 258 /* translators: %s: percentage of this month's emails used */ 259 printf( __( "You've used %s%% of this month's emails.", 'mailhawk' ), number_format_i18n( $percent_usage ) ); 260 ?></b> 249 261 <?php _e( 'Upgrade your plan before you run out.', 'mailhawk' ); ?> 250 262 <?php endif; ?> -
mailhawk/tags/1.5.2/admin/views/suspended.php
r3793271 r3795449 47 47 <?php endif; ?> 48 48 <?php if ( $since ) : ?> 49 <p><?php printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); ?></p> 49 <p><?php 50 /* translators: %s: date and time the account was suspended */ 51 printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); 52 ?></p> 50 53 <?php endif; ?> 51 54 <?php if ( 'organization' === $suspension['scope'] ) : ?> 52 55 <p><?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?></p> 53 56 <?php endif; ?> 54 <p><?php _e( 'Your MailHawk account page says what to do next, for example to renew the subscription or to sendan incident report.', 'mailhawk' ); ?></p>57 <p><?php _e( 'Your MailHawk account page says what to do next, for example, renewing your subscription or sending an incident report.', 'mailhawk' ); ?></p> 55 58 <a class="button button-primary big-button mailhawk-cta" href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener"> 56 59 <span class="dashicons dashicons-external" aria-hidden="true"></span> -
mailhawk/tags/1.5.2/admin/views/test.php
r2309454 r3795449 1 1 <?php 2 2 3 use MailHawk\Api\Postal\Domains; 4 use function MailHawk\get_array_var; 5 use function MailHawk\get_default_from_email_address; 6 use function MailHawk\get_test_email_faq; 7 use function MailHawk\get_test_from_addresses; 8 use function MailHawk\mailhawk_admin_page; 9 10 // The domains MailHawk knows, to suggest addresses to send from and to warn about ones that are not authenticated 11 $domains = Domains::query_all(); 12 $from = get_test_from_addresses( is_wp_error( $domains ) ? [] : $domains ); 13 14 // What was sent last stays, the default from address the first time 15 $from_address = get_array_var( $this->test_data, 'from' ) ?: strtolower( (string) get_default_from_email_address() ); 16 3 17 ?> 4 <div class="mailhawk-content-box domains ">18 <div class="mailhawk-content-box domains mailhawk-test-email"> 5 19 <p><b><?php _e( 'Send a test email:', 'mailhawk' ); ?></b></p> 6 20 7 21 <form method="post"> 8 22 <?php wp_nonce_field( 'send_test_email', '_mailhawk_nonce' ); ?> 23 <?php 24 25 // The default first, then the addresses on verified domains, then the rest 26 $groups = [ 'default' => [], 'verified' => [], 'unverified' => [] ]; 27 28 foreach ( $from['addresses'] as $address ) { 29 $groups[ $address['default'] ? 'default' : ( $address['authenticated'] ? 'verified' : 'unverified' ) ][] = $address['email']; 30 } 31 32 $suggested = in_array( $from_address, wp_list_pluck( $from['addresses'], 'email' ), true ); 33 34 ?> 9 35 <p> 10 <input type="text" name="subject" class="regular-text" placeholder="<?php esc_attr_e( 'Subject line...', 'mailhawk' ); ?>" value="<?php esc_attr_e( 'MailHawk is connected!', 'mailhawk' );?>" > 36 <label for="mailhawk-test-from-select"><?php _e( 'From', 'mailhawk' ); ?></label><br> 37 <select id="mailhawk-test-from-select" class="regular-text" hidden> 38 <?php foreach ( $groups['default'] as $email ) : ?> 39 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php 40 /* translators: %s: the default from email address */ 41 echo esc_html( sprintf( __( '%s (default)', 'mailhawk' ), $email ) ); 42 ?></option> 43 <?php endforeach; ?> 44 <?php if ( $groups['verified'] ) : ?> 45 <optgroup label="<?php esc_attr_e( 'Verified domains', 'mailhawk' ); ?>"> 46 <?php foreach ( $groups['verified'] as $email ) : ?> 47 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php echo esc_html( $email ); ?></option> 48 <?php endforeach; ?> 49 </optgroup> 50 <?php endif; ?> 51 <?php if ( $groups['unverified'] ) : ?> 52 <optgroup label="<?php esc_attr_e( 'Domains that are not verified', 'mailhawk' ); ?>"> 53 <?php foreach ( $groups['unverified'] as $email ) : ?> 54 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php echo esc_html( $email ); ?></option> 55 <?php endforeach; ?> 56 </optgroup> 57 <?php endif; ?> 58 <option value="" <?php selected( ! $suggested ); ?>><?php _e( 'Other address…', 'mailhawk' ); ?></option> 59 </select> 60 <?php // What is sent. Without JavaScript it is the field, with it the select fills it in and it is only shown for another address. ?> 61 <input type="email" name="from_address" id="mailhawk-test-from" class="regular-text" 62 aria-label="<?php esc_attr_e( 'From address', 'mailhawk' ); ?>" 63 placeholder="<?php esc_attr_e( 'name@yourdomain.com', 'mailhawk' ); ?>" 64 value="<?php echo esc_attr( $from_address ); ?>" 65 data-domains="<?php echo esc_attr( wp_json_encode( (object) $from['domains'] ) ); ?>"> 11 66 </p> 67 <div class="mailhawk-test-from-warning notice notice-warning inline" hidden 68 data-unverified="<?php 69 /* translators: %s: the domain of the from address */ 70 echo esc_attr__( 'The DNS records of %s are not all verified, so this email may go to spam or be refused.', 'mailhawk' ); 71 ?>" 72 data-unknown="<?php 73 /* translators: %s: the domain of the from address */ 74 echo esc_attr__( '%s is not one of your MailHawk domains, so this email can not be authenticated and may go to spam or be refused.', 'mailhawk' ); 75 ?>" 76 data-dns-url="<?php echo esc_attr( mailhawk_admin_page( [ 'view' => 'domains', 'domain' => '%s' ] ) ); ?>" 77 data-domains-url="<?php echo esc_attr( mailhawk_admin_page( [ 'view' => 'domains' ] ) ); ?>"> 78 <p> 79 <span class="mailhawk-test-from-warning-text"></span> 80 <a class="mailhawk-test-from-warning-link" href="#" 81 data-unverified="<?php esc_attr_e( 'Check the DNS records', 'mailhawk' ); ?>" 82 data-unknown="<?php esc_attr_e( 'Add the domain', 'mailhawk' ); ?>"></a> 83 </p> 84 </div> 12 85 <p> 13 <textarea name="content" class="regular-text" rows="4" placeholder="<?php esc_attr_e( 'Content...', 'mailhawk' ); ?>"><?php esc_html_e( 'Hello World!', 'mailhawk' ); ?></textarea> 14 </p> 15 <p> 16 <input type="email" name="to_address" class="regular-text" 86 <label for="mailhawk-test-to"><?php _e( 'To', 'mailhawk' ); ?></label><br> 87 <input type="email" name="to_address" id="mailhawk-test-to" class="regular-text" 17 88 value="<?php esc_attr_e( wp_get_current_user()->user_email ); ?>"> 18 89 <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Send Test' ); ?>"> 19 90 </p> 20 91 </form> 21 <p class="description"><?php _e( ' Send a test email to yourself to ensure MailHawk is properly connected and your deliverability is good.', 'mailhawk' ); ?></p>92 <p class="description"><?php _e( 'MailHawk sends a short email that confirms the site can send, with tips on what it means for where it lands. Send it to an address of your own at Gmail, Outlook or wherever your customers are, and see where it ends up.', 'mailhawk' ); ?></p> 22 93 23 94 </div> 95 <div class="mailhawk-content-box mailhawk-test-faq"> 96 <p><b><?php _e( 'Where did it land?', 'mailhawk' ); ?></b></p> 97 <?php foreach ( get_test_email_faq( true ) as $item ) : ?> 98 <details> 99 <summary><?php echo esc_html( $item['question'] ); ?></summary> 100 <p><?php echo wp_kses( $item['answer'], [ 'a' => [ 'href' => true ], 'b' => [], 'i' => [] ] ); ?></p> 101 </details> 102 <?php endforeach; ?> 103 </div> -
mailhawk/tags/1.5.2/assets/css/admin.css
r3793271 r3795449 1108 1108 1109 1109 .not-sure-dns { 1110 border-color: #e1e5ea; 1110 1111 border-radius: 0 0 10px 10px; 1112 } 1113 1114 /* The test email */ 1115 .mailhawk-test-email label { 1116 font-weight: 600; 1117 } 1118 1119 .mailhawk-test-email .mailhawk-test-from-warning { 1120 max-width: 25em; 1121 margin: -4px 0 12px; 1122 } 1123 1124 .mailhawk-test-email .mailhawk-test-from-warning[hidden], 1125 .mailhawk-test-email [hidden] { 1126 display: none; 1127 } 1128 1129 /* The field for another address sits under the select */ 1130 .mailhawk-test-email #mailhawk-test-from-select + #mailhawk-test-from { 1131 display: block; 1132 margin-top: 8px; 1133 } 1134 1135 .mailhawk-test-email #mailhawk-test-from-select + #mailhawk-test-from[hidden] { 1136 display: none; 1137 } 1138 1139 /* Where the test email landed, one question open at a time is plenty */ 1140 .mailhawk-test-faq details { 1141 border-top: 1px solid #e1e5ea; 1142 } 1143 1144 .mailhawk-test-faq details:last-child { 1145 border-bottom: 1px solid #e1e5ea; 1146 } 1147 1148 .mailhawk-test-faq summary { 1149 padding: 12px 0; 1150 font-weight: 600; 1151 cursor: pointer; 1152 } 1153 1154 .mailhawk-test-faq details p { 1155 max-width: 50em; 1156 margin: 0 0 14px; 1157 color: #50575e; 1158 } 1159 1160 /* Handing the DNS records to someone else */ 1161 .not-sure-dns .mailhawk-dns-handoff { 1162 margin: 0; 1163 padding: 12px 0; 1164 border-top: 1px solid #e1e5ea; 1165 color: #50575e; 1166 } 1167 1168 /* The ID matches the rule that makes every button in a content box a block */ 1169 #wpbody .mailhawk-content-box .mailhawk-copy-button.button { 1170 display: inline-flex; 1171 align-items: center; 1172 gap: 4px; 1173 margin-left: 6px; 1174 vertical-align: middle; 1175 } 1176 1177 .mailhawk-copy-button .dashicons { 1178 font-size: 16px; 1179 width: 16px; 1180 height: 16px; 1181 } 1182 1183 .mailhawk-copy-button.button.copied, 1184 .mailhawk-copy-button.button.copied:hover, 1185 .mailhawk-copy-button.button.copied:focus { 1186 border-color: #2ba123; 1187 background: #e3f5e8; 1188 color: #146c2e; 1189 } 1190 1191 /* The DNS table and the help under it are one card, so the table only rounds its top corners */ 1192 .mailhawk-view-content .wp-list-table.dns-list { 1193 border-bottom-left-radius: 0; 1194 border-bottom-right-radius: 0; 1111 1195 } 1112 1196 … … 1627 1711 } 1628 1712 } 1713 1714 /* DNS record names: the full hostname as a pill, with the host as a pill inside it */ 1715 .mailhawk-dns-name { 1716 display: flex; 1717 align-items: center; 1718 flex-wrap: wrap; 1719 gap: 0; 1720 box-sizing: border-box; 1721 width: 100%; 1722 min-height: 40px; 1723 padding: 4px 10px 4px 4px; 1724 border: 1px solid #cfd6de; 1725 border-radius: 6px; 1726 background: #f6f7f7; 1727 color: #1d2327; 1728 font-family: Consolas, Monaco, monospace; 1729 font-size: 14px; 1730 line-height: 1.4; 1731 cursor: copy; 1732 word-break: break-all; 1733 transition: border-color .15s, background-color .15s; 1734 } 1735 1736 .mailhawk-dns-name:hover, 1737 .mailhawk-dns-name:focus-visible { 1738 border-color: #2271b1; 1739 background: #f0f6fc; 1740 outline: none; 1741 } 1742 1743 .mailhawk-dns-name .mailhawk-dns-host { 1744 padding: 3px 7px; 1745 border: 1px solid #2271b1; 1746 border-radius: 4px; 1747 background: #fff; 1748 color: #2271b1; 1749 font: inherit; 1750 cursor: copy; 1751 transition: background-color .15s, color .15s; 1752 } 1753 1754 .mailhawk-dns-name .mailhawk-dns-host:hover, 1755 .mailhawk-dns-name .mailhawk-dns-host:focus-visible { 1756 background: #2271b1; 1757 color: #fff; 1758 outline: none; 1759 } 1760 1761 .mailhawk-dns-name .mailhawk-dns-rest { 1762 padding-left: 1px; 1763 color: #50575e; 1764 } 1765 1766 /* The domain itself, shown after the @ */ 1767 .mailhawk-dns-name .mailhawk-dns-rest.is-domain { 1768 padding-left: 8px; 1769 } 1770 1771 .mailhawk-dns-name .dashicons { 1772 margin-left: auto; 1773 padding-left: 8px; 1774 color: #8c8f94; 1775 font-size: 16px; 1776 line-height: 20px; 1777 } 1778 1779 .mailhawk-dns-name:hover .dashicons { 1780 color: #2271b1; 1781 } 1782 1783 .mailhawk-dns-name .mailhawk-dns-copied { 1784 margin-left: auto; 1785 padding-left: 8px; 1786 color: #146c2e; 1787 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 1788 font-size: 12px; 1789 white-space: nowrap; 1790 } 1791 1792 .mailhawk-dns-name .mailhawk-dns-copied:empty { 1793 display: none; 1794 } 1795 1796 /* Copied: the icon makes way for what was copied, and the part that was copied shows it */ 1797 .mailhawk-dns-name.copied-host .dashicons, 1798 .mailhawk-dns-name.copied-hostname .dashicons { 1799 display: none; 1800 } 1801 1802 .mailhawk-dns-name.copied-hostname { 1803 border-color: #2ba123; 1804 background: #e3f5e8; 1805 } 1806 1807 .mailhawk-dns-name.copied-host .mailhawk-dns-host { 1808 border-color: #2ba123; 1809 background: #2ba123; 1810 color: #fff; 1811 } 1812 1813 /* The TTL and value: one line, a long value is cut short on screen but copied whole */ 1814 .mailhawk-dns-name.mailhawk-copy-value { 1815 flex-wrap: nowrap; 1816 padding-left: 10px; 1817 text-align: left; 1818 word-break: normal; 1819 } 1820 1821 .mailhawk-copy-value .mailhawk-copy-text { 1822 flex: 1 1 auto; 1823 min-width: 0; 1824 overflow: hidden; 1825 text-overflow: ellipsis; 1826 white-space: nowrap; 1827 } -
mailhawk/tags/1.5.2/assets/js/admin.js
r3788980 r3795449 1 (function ($, connect, review, groundhogg) { 1 (function ($, connect, review, groundhogg, dns) { 2 3 // Copy text, with a fallback for wp-admin served without HTTPS, where the clipboard API is not there 4 function copyText(text) { 5 if (navigator.clipboard && window.isSecureContext) { 6 return navigator.clipboard.writeText(text); 7 } 8 9 var $field = $('<textarea readonly>').val(text).css({position: 'fixed', top: 0, left: 0, opacity: 0}).appendTo('body'); 10 $field[0].select(); 11 12 var copied = document.execCommand('copy'); 13 $field.remove(); 14 15 return copied ? $.Deferred().resolve().promise() : $.Deferred().reject().promise(); 16 } 17 18 // The names on the DNS screen: the host inside copies just the host, the rest of the pill the full hostname. The 19 // TTL and value pills copy what they show. 20 $(document).on('click', '.mailhawk-dns-name, .mailhawk-dns-host', function (e) { 21 e.stopPropagation(); 22 23 var $target = $(this); 24 var $name = $target.closest('.mailhawk-dns-name'); 25 var isHost = $target.is('.mailhawk-dns-host'); 26 27 $.when(copyText($target.attr('data-copy'))).done(function () { 28 // A value says it in its own words, a name says which part 29 $name.find('.mailhawk-dns-copied').text($name.attr('data-copied') || (isHost ? dns.host_copied : dns.hostname_copied)); 30 $name.toggleClass('copied-host', isHost).toggleClass('copied-hostname', !isHost); 31 32 clearTimeout($name.data('copiedTimer')); 33 $name.data('copiedTimer', setTimeout(function () { 34 $name.removeClass('copied-host copied-hostname').find('.mailhawk-dns-copied').text(''); 35 }, 1500)); 36 }); 37 }); 38 39 // A button that copies a block of text, like the DNS instructions, and says so on itself for a moment 40 $(document).on('click', '.mailhawk-copy-button', function () { 41 var $button = $(this); 42 var $label = $button.find('.mailhawk-copy-button-label'); 43 44 if (!$button.data('label')) { 45 $button.data('label', $label.text()); 46 } 47 48 $.when(copyText($button.attr('data-copy'))).done(function () { 49 $label.text($button.attr('data-copied')); 50 $button.addClass('copied'); 51 52 clearTimeout($button.data('copiedTimer')); 53 $button.data('copiedTimer', setTimeout(function () { 54 $label.text($button.data('label')); 55 $button.removeClass('copied'); 56 }, 2000)); 57 }); 58 }); 59 60 // The test email: say so when the address it is sent from is on a domain that is not verified, or not MailHawk's 61 $(function () { 62 var $from = $('#mailhawk-test-from'); 63 var $warning = $('.mailhawk-test-from-warning'); 64 65 if (!$from.length || !$warning.length) { 66 return; 67 } 68 69 var domains = $from.data('domains') || {}; 70 var $link = $warning.find('.mailhawk-test-from-warning-link'); 71 var $select = $('#mailhawk-test-from-select'); 72 73 // The select picks a suggested address, its last choice opens the field for any other 74 function pick() { 75 var value = $select.val(); 76 77 $from.prop('hidden', value !== ''); 78 79 if (value !== '') { 80 $from.val(value); 81 } else if ($select.find('option[value="' + $from.val() + '"]').length) { 82 // Coming from a suggestion, start the other address empty 83 $from.val('').trigger('focus'); 84 } 85 86 check(); 87 } 88 89 $select.prop('hidden', false).on('change', pick); 90 91 function check() { 92 var email = $.trim($from.val()).toLowerCase(); 93 var at = email.lastIndexOf('@'); 94 var domain = at > 0 ? email.slice(at + 1) : ''; 95 96 // Nothing to say until there is an address to say it about, or when its domain is verified 97 if (!domain || domain.indexOf('.') === -1 || domains[domain] === true) { 98 $warning.prop('hidden', true); 99 return; 100 } 101 102 var known = Object.prototype.hasOwnProperty.call(domains, domain); 103 var kind = known ? 'unverified' : 'unknown'; 104 105 $warning.find('.mailhawk-test-from-warning-text').text($warning.attr('data-' + kind).replace('%s', domain)); 106 $link.text($link.attr('data-' + kind)).attr('href', known 107 ? $warning.attr('data-dns-url').replace('%s', encodeURIComponent(domain)) 108 : $warning.attr('data-domains-url')); 109 110 $warning.prop('hidden', false); 111 } 112 113 $from.on('input change', check); 114 pick(); 115 }); 116 117 // The pill is not a button element, it holds one, so Enter and Space are handled here 118 $(document).on('keydown', '.mailhawk-dns-name', function (e) { 119 if (e.target === this && (e.key === 'Enter' || e.key === ' ')) { 120 e.preventDefault(); 121 $(this).trigger('click'); 122 } 123 }); 2 124 3 125 // The suggestions to look at Groundhogg, on the overview and the recipients screen. Learn more opens WordPress' own … … 133 255 134 256 135 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg );257 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg, MailHawkDns); -
mailhawk/tags/1.5.2/includes/abilities/ability.php
r3793271 r3795449 134 134 135 135 if ( mailhawk_is_suspended() ) { 136 return $this->error( 'suspended', __( 'The MailHawk account is suspended , mailhawk/get-status says why and where to go next.', 'mailhawk' ), 409 );136 return $this->error( 'suspended', __( 'The MailHawk account is suspended. Use mailhawk/get-status to see why and what to do next.', 'mailhawk' ), 409 ); 137 137 } 138 138 -
mailhawk/tags/1.5.2/includes/abilities/email/send-test-email.php
r3788980 r3795449 6 6 use MailHawk\Plugin; 7 7 use WP_Error; 8 use function MailHawk\get_test_email; 9 use function MailHawk\set_next_alt_body; 8 10 9 11 if ( ! defined( 'ABSPATH' ) ) { … … 83 85 } 84 86 85 $site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );86 $ subject = sprintf( __( 'MailHawk test email from %s', 'mailhawk' ), $site);87 $ message = sprintf( __( "This is a test email from %s, sent through MailHawk to check that email is delivered.\n\nYou can ignore it.", 'mailhawk' ), $site );87 // The same email the Test screen sends 88 $email = get_test_email( $to ); 89 $subject = $email['subject']; 88 90 89 91 $table = Plugin::instance()->log->get_table_name(); … … 98 100 add_action( 'wp_mail_failed', $catch ); 99 101 102 $alt_body = set_next_alt_body( $email['text'] ); 103 100 104 // Straight to MailHawk's mailer, which is what wp_mail() is while the site is connected. A test that went through 101 105 // whatever else is hooked in would not say anything about MailHawk. 102 106 try { 103 $sent = mailhawk_mail( $to, $subject, $ message);107 $sent = mailhawk_mail( $to, $subject, $email['html'], [ 'Content-Type: text/html' ] ); 104 108 } catch ( \Exception $e ) { 105 109 $sent = false; … … 107 111 } finally { 108 112 remove_action( 'wp_mail_failed', $catch ); 113 remove_action( 'phpmailer_init', $alt_body, 99 ); 109 114 } 110 115 -
mailhawk/tags/1.5.2/includes/abilities/formatter.php
r3788980 r3795449 4 4 5 5 use MailHawk\DB\Recipients; 6 use function MailHawk\get_dns_record_names; 6 7 use function MailHawk\get_email_status_pretty_name; 7 8 use function MailHawk\get_spf_record; … … 187 188 } 188 189 190 // What goes in the Host field, relative to the domain, and the full hostname, as the DNS screen shows them 191 $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name ?? '', (string) $domain->name ); 192 $return_path_names = get_dns_record_names( $domain->return_path->return_path_domain ?? '', (string) $domain->name ); 193 $spf_names = get_dns_record_names( '@', (string) $domain->name ); 194 189 195 return [ 190 196 'records' => [ … … 192 198 'purpose' => 'dkim', 193 199 'type' => 'TXT', 200 'host' => $dkim_names['host'], 201 'hostname' => $dkim_names['hostname'], 194 202 'name' => strtolower( (string) ( $domain->dkim->dkim_record_name ?? '' ) ), 195 203 'value' => (string) ( $domain->dkim->dkim_record ?? '' ), … … 201 209 'purpose' => 'return_path', 202 210 'type' => 'CNAME', 211 'host' => $return_path_names['host'], 212 'hostname' => $return_path_names['hostname'], 203 213 'name' => strtolower( (string) ( $domain->return_path->return_path_domain ?? '' ) ), 204 214 'value' => self::RETURN_PATH_TARGET, … … 210 220 'purpose' => 'spf', 211 221 'type' => 'TXT', 222 'host' => $spf_names['host'], 223 'hostname' => $spf_names['hostname'], 212 224 'name' => '@', 213 225 'value' => $spf_record, … … 231 243 'records' => [ 232 244 'type' => 'array', 233 'description' => __( 'The records to add at the DNS host of the domain, as they are shown on the MailHawk DNS screen. A name of @ is the domain itself.', 'mailhawk' ),245 'description' => __( 'The records to add at the DNS host of the domain, as they are shown on the MailHawk DNS screen. host is what goes in the Host or Name field, relative to the domain, and @ is the domain itself. hostname is the full name. For a subdomain whose records are added in the DNS of its parent domain, the subdomain is added to host. name is kept for compatibility: the DKIM name is relative and the return path name is full.', 'mailhawk' ), 234 246 'items' => [ 235 247 'type' => 'object', … … 243 255 'enum' => [ 'TXT', 'CNAME' ], 244 256 ], 257 'host' => [ 'type' => 'string' ], 258 'hostname' => [ 'type' => 'string' ], 245 259 'name' => [ 'type' => 'string' ], 246 260 'value' => [ 'type' => 'string' ], … … 249 263 'verified' => [ 'type' => 'boolean' ], 250 264 ], 251 'required' => [ 'purpose', 'type', ' name', 'value', 'ttl', 'status', 'verified' ],265 'required' => [ 'purpose', 'type', 'host', 'hostname', 'name', 'value', 'ttl', 'status', 'verified' ], 252 266 ], 253 267 ], -
mailhawk/tags/1.5.2/includes/abilities/status/get-status.php
r3793271 r3795449 238 238 239 239 $message = $details['reason'] 240 /* translators: %s: the reason MailHawk gave for suspending the account */ 240 241 ? sprintf( __( 'The MailHawk account is suspended: %s', 'mailhawk' ), $details['reason'] ) 241 242 : __( 'The MailHawk account is suspended.', 'mailhawk' ); 242 243 244 /* translators: %s: URL of the MailHawk account page */ 243 245 $issues[] = [ 'code' => 'suspended', 'message' => $message . ' ' . sprintf( __( 'What to do next is on the MailHawk account page, %s, where a person has to sign in.', 'mailhawk' ), mailhawk_account_url() ) ]; 244 246 } … … 257 259 258 260 if ( $usage && $usage['percent'] >= 90 ) { 261 /* translators: 1: percentage of the monthly allowance used, 2: emails left */ 259 262 $issues[] = [ 'code' => 'usage_high', 'message' => sprintf( __( '%1$s%% of the monthly sending allowance is used, %2$s emails are left.', 'mailhawk' ), $usage['percent'], number_format_i18n( $usage['remaining'] ) ) ]; 260 263 } -
mailhawk/tags/1.5.2/includes/api/postal/messages.php
r3788980 r3795449 62 62 63 63 if ( ! $api_key ) { 64 return new \WP_Error( 'mailhawk_rejected', 'Invalid API key.');64 return new \WP_Error( 'mailhawk_rejected', __( 'Invalid API key.', 'mailhawk' ) ); 65 65 } 66 66 -
mailhawk/tags/1.5.2/includes/api/postal/postal.php
r3793271 r3795449 53 53 54 54 if ( ! $api_key ){ 55 return new \WP_Error( 'error', 'Invalid API key.');55 return new \WP_Error( 'error', __( 'Invalid API key.', 'mailhawk' ) ); 56 56 } 57 57 … … 93 93 return new \WP_Error( 94 94 'mailhawk_rate_limited', 95 __( 'MailHawk is slowing down email from this site, usually because the monthly sending limit of the plan is reached. Upgrade the plan, or wait for the limit to reset.', 'mailhawk' ),95 __( "MailHawk is slowing down email from this site, usually because the plan's monthly sending limit has been reached. Upgrade the plan, or wait for the limit to reset.", 'mailhawk' ), 96 96 [ 'retry_after' => $retry_after ? $retry_after : 5 * MINUTE_IN_SECONDS ] 97 97 ); 98 98 } 99 99 100 $result = new \WP_Error( 'error', 'Unable to complete request.');100 $result = new \WP_Error( 'error', __( 'Unable to complete request.', 'mailhawk' ) ); 101 101 102 102 // Postal … … 108 108 break; 109 109 case 'parameter-error': 110 $result = new \WP_Error( 'parameter-error', $json->data->message ?? 'Invalid request.');110 $result = new \WP_Error( 'parameter-error', $json->data->message ?? __( 'Invalid request.', 'mailhawk' ) ); 111 111 break; 112 112 case 'error': … … 120 120 $message = __( 'MailHawk has suspended this account.', 'mailhawk' ); 121 121 } else { 122 $message = sprintf( 'MailHawk returned an error: %s', $code ); 122 /* translators: %s: error code returned by MailHawk */ 123 $message = sprintf( __( 'MailHawk returned an error: %s', 'mailhawk' ), $code ); 123 124 } 124 125 -
mailhawk/tags/1.5.2/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; -
mailhawk/tags/1.5.2/includes/quarantine.php
r3788980 r3795449 104 104 ] ); 105 105 106 // Someone must have take care of it already106 // Someone must have taken care of it already 107 107 if ( $count_quarantined === 0 ) { 108 108 return; -
mailhawk/tags/1.5.2/includes/site-health.php
r3794810 r3795449 132 132 'recommended', 133 133 __( 'MailHawk is not connected', 'mailhawk' ), 134 __( 'Email from this site is not going out through MailHawk, so it is sent the way WordPress does it by default, and itmay not reach the inbox.', 'mailhawk' ),134 __( 'Email from this site is not going out through MailHawk, so it is sent with the default WordPress mailer and may not reach the inbox.', 'mailhawk' ), 135 135 $this->link( mailhawk_admin_page(), __( 'Connect MailHawk', 'mailhawk' ) ) 136 136 ); … … 143 143 $description = [ __( 'The MailHawk service is not accepting email from this site, so none of it is going out.', 'mailhawk' ) ]; 144 144 145 // Older MTA builds do not say why 145 // Older MTA builds do not say why. The reason may not end in a period, so it gets its own. 146 146 $description[] = $suspension['reason'] 147 ? sprintf( __( 'The reason given: %s', 'mailhawk' ), $suspension['reason'] ) 147 /* translators: %s: the reason MailHawk gave for suspending the account */ 148 ? sprintf( __( 'Reason: %s.', 'mailhawk' ), rtrim( $suspension['reason'], '. ' ) ) 148 149 : __( 'This is usually a problem with the account or the subscription.', 'mailhawk' ); 149 150 150 151 if ( $since ) { 152 /* translators: %s: date and time the account was suspended */ 151 153 $description[] = sprintf( __( 'Suspended since %s.', 'mailhawk' ), $since ); 152 154 } … … 200 202 'critical', 201 203 __( 'MailHawk is turning email away', 'mailhawk' ), 202 __( "This site has used all of its emails for the month, so MailHawk is not sending what it is asked to. The emails it turned away are marked as failed in the log. Sending starts again when the limit resets, or as soon as the plan is upgraded.", 'mailhawk' ),204 __( "This site has used all of its emails for the month, so MailHawk is not sending new email. The emails it turned away are marked as failed in the log. Sending starts again when the limit resets, or as soon as the plan is upgraded.", 'mailhawk' ), 203 205 $this->upgrade_link() . $this->link( mailhawk_admin_page( [ 'view' => 'log', 'status' => 'failed' ] ), __( 'See the failed emails', 'mailhawk' ) ) 204 206 ); … … 309 311 'critical', 310 312 __( 'This site used all of its emails for the month', 'mailhawk' ), 313 /* translators: 1: emails sent this month, 2: emails in the plan */ 311 314 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan. MailHawk turns email away until the limit resets.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ), 312 315 $this->upgrade_link() … … 319 322 'recommended', 320 323 __( 'This site is close to the limit of its plan', 'mailhawk' ), 324 /* translators: 1: emails sent this month, 2: emails in the plan */ 321 325 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan this month. When they are used up, MailHawk turns email away until the limit resets.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ), 322 326 $this->upgrade_link() … … 328 332 'good', 329 333 __( 'This site has emails left this month', 'mailhawk' ), 334 /* translators: 1: emails sent this month, 2: emails in the plan */ 330 335 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan this month.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ) 331 336 ); -
mailhawk/tags/1.5.2/mailhawk.php
r3794810 r3795449 5 5 * Plugin URI: https://mailhawk.io 6 6 * Description: Send better email that will reach the inbox with MailHawk. 7 * Version: 1.5. 17 * Version: 1.5.2 8 8 * Requires at least: 6.9 9 9 * Requires PHP: 7.4 … … 26 26 if ( ! defined( 'ABSPATH' ) ) exit; 27 27 28 define( 'MAILHAWK_VERSION', '1.5. 1' );29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5. 0' );28 define( 'MAILHAWK_VERSION', '1.5.2' ); 29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.1' ); 30 30 define( 'MAILHAWK_LICENSE_SERVER_URL', 'https://mailhawk.io' ); 31 31 -
mailhawk/tags/1.5.2/readme.txt
r3794810 r3795449 5 5 Requires at least: 6.9 6 6 Tested up to: 7.1 7 Stable tag: 1.5. 17 Stable tag: 1.5.2 8 8 Requires PHP: 7.4 9 9 License: GPLv3 or later … … 167 167 168 168 == Changelog == 169 170 = 1.5.2 (2026-11-10) = 171 * ADDED The test email can be sent from any address. A dropdown suggests the default from address and the same name at each of your domains, and a warning says when the domain of the address you pick is not verified, or not one of your MailHawk domains. 172 * IMPROVED The test email is now a branded email that says what it means for where it landed: the inbox, Gmail's Updates or Promotions tabs, Outlook's Other folder, or spam. The Test screen has the same tips, and what to do when it did not arrive at all. 173 * ADDED A "Copy instructions" button on the DNS screen copies everything someone else needs to add the records, like an agency, a developer or an AI assistant: every record with its host, full hostname, value and TTL, what to do with an SPF record that is already there, and where to verify. 174 * IMPROVED Record names on the DNS screen copy with one click: the highlighted host copies just the host, anywhere else copies the full hostname. The screen also explains what to enter when the records of a subdomain go in the DNS of its parent domain, and the host of the return path no longer includes your domain, so DNS providers that add it themselves don't end up with it twice. 175 * TWEAKED Clearer wording on the overview, in Site Health, and on the suspended screen and notice. When the monthly limit is reached, the notice on the overview is titled "Your monthly email limit is reached". 176 * FIXED Some error messages and the usage line on the overview could not be translated. 177 * DEV The `mailhawk/get-domain-dns` ability returns the `host` and the full `hostname` of each record. 178 * DEV New filter `mailhawk/second_level_suffixes` lists the suffixes like co.uk that domains are registered under, so the DNS screen does not take example.co.uk for a subdomain. 169 179 170 180 = 1.5.1 (2026-11-10) = -
mailhawk/tags/1.5.2/uninstall.php
r3793271 r3795449 7 7 * @package MailHawk 8 8 * @subpackage Uninstall 9 * @copyright Copyright (c) 2015, Pippin Williamson10 * @license http ://opensource.org/licenses/gpl-2.0.php GNU Public License11 * @since 1. 4.39 * @copyright Copyright (c) MailHawk Inc. 10 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 or later 11 * @since 1.0.0 12 12 */ 13 13 … … 15 15 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit; 16 16 17 // Load WPGH file.17 // Load the plugin so its tables can be dropped. 18 18 include_once __DIR__ . '/mailhawk.php'; 19 19 -
mailhawk/trunk/admin/admin.php
r3794810 r3795449 15 15 use function MailHawk\dismiss_notice; 16 16 use function MailHawk\get_array_var; 17 use function MailHawk\get_test_email; 18 use function MailHawk\set_next_alt_body; 17 19 use function MailHawk\get_dismissible_notices; 18 20 use function MailHawk\get_hostname; … … 579 581 } 580 582 581 $to = sanitize_email( get_post_var( 'to_address' ) ); 582 $subject = sanitize_text_field( get_post_var( 'subject' ) ); 583 $body = sanitize_textarea_field( get_post_var( 'content' ) ); 584 585 $this->test_data['to'] = $to; 586 $this->test_data['subject'] = $subject; 587 $this->test_data['body'] = $body; 583 $to = sanitize_email( get_post_var( 'to_address' ) ); 584 $from = sanitize_email( get_post_var( 'from_address' ) ); 585 586 $this->test_data['to'] = $to; 587 $this->test_data['from'] = $from; 588 588 589 589 if ( ! $to ) { … … 593 593 add_action( 'wp_mail_failed', [ $this, 'maybe_email_failed' ] ); 594 594 595 $success = wp_mail( $to, $subject, $body, [ 596 'sender' => 'app@mailhawk.io' 597 ] ); 595 // Always the same email, what is being tested is whether it arrives and where 596 $email = get_test_email( $to, $from ); 597 $headers = [ 'Content-Type: text/html' ]; 598 599 // From the address that was picked, with the default from name. Without one it is the default from address. 600 if ( $from ) { 601 $name = (string) get_option( 'mailhawk_default_from_name' ); 602 $headers[] = $name ? sprintf( 'From: %s <%s>', $name, $from ) : sprintf( 'From: %s', $from ); 603 } 604 605 $alt_body = set_next_alt_body( $email['text'] ); 606 607 $success = wp_mail( $to, $email['subject'], $email['html'], $headers ); 608 609 remove_action( 'phpmailer_init', $alt_body, 99 ); 598 610 599 611 if ( $success ) { … … 1100 1112 wp_localize_script( 'mailhawk-admin', 'MailHawkConnect', [ 1101 1113 'connecting_text' => '<span class="dashicons dashicons-admin-generic"></span>' . __( 'Connecting You To MailHawk...', 'mailhawk' ) 1114 ] ); 1115 1116 // What the DNS screen says when a name is copied 1117 wp_localize_script( 'mailhawk-admin', 'MailHawkDns', [ 1118 'host_copied' => __( 'Host copied', 'mailhawk' ), 1119 'hostname_copied' => __( 'Full hostname copied', 'mailhawk' ), 1102 1120 ] ); 1103 1121 … … 1403 1421 <p> 1404 1422 <?php if ( $suspension['reason'] ) : ?> 1405 <?php printf( __( '<b>Attention:</b> MailHawk has suspended your account, so email from this site is not being sent. The reason given: %s', 'mailhawk' ), esc_html( $suspension['reason'] ) ); ?> 1423 <?php 1424 /* translators: %s: the reason MailHawk gave for suspending the account */ 1425 printf( __( '<b>Attention:</b> MailHawk has suspended your account, so email from this site is not being sent. Reason: %s', 'mailhawk' ), esc_html( $suspension['reason'] ) ); 1426 ?> 1406 1427 <?php else : ?> 1407 1428 <?php _e( '<b>Attention:</b> It looks like your account has been suspended. To continue sending email you must reactivate your account.', 'mailhawk' ); ?> 1408 1429 <?php endif; ?> 1409 <?php if ( $since ) : ?>1410 <?php printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); ?>1411 <?php endif; ?>1412 <?php if ( 'organization' === $suspension['scope'] ) : ?>1413 <?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?>1414 <?php endif; ?>1415 1430 </p> 1431 <?php if ( $since || 'organization' === $suspension['scope'] ) : ?> 1432 <p> 1433 <?php if ( $since ) : ?> 1434 <?php 1435 /* translators: %s: date and time the account was suspended */ 1436 printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); 1437 ?> 1438 <?php endif; ?> 1439 <?php if ( 'organization' === $suspension['scope'] ) : ?> 1440 <?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?> 1441 <?php endif; ?> 1442 </p> 1443 <?php endif; ?> 1416 1444 <p> 1417 1445 <a href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener" … … 1549 1577 ?> 1550 1578 <div class="notice notice-success is-dismissible"> 1551 <p><?php printf( __( 'We sent a test email to %s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>" ); ?></p> 1579 <p><?php 1580 if ( get_array_var( $this->test_data, 'from' ) ) { 1581 /* translators: 1: recipient of the test email, 2: the address it was sent from */ 1582 printf( __( 'We sent a test email to %1$s from %2$s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>", "<code>" . esc_html( get_array_var( $this->test_data, 'from' ) ) . "</code>" ); 1583 } else { 1584 /* translators: %s: recipient of the test email */ 1585 printf( __( 'We sent a test email to %s.', 'mailhawk' ), "<code>" . esc_html( get_array_var( $this->test_data, 'to' ) ) . "</code>" ); 1586 } 1587 ?></p> 1552 1588 </div> 1553 1589 <?php -
mailhawk/trunk/admin/views/dns-single.php
r3788980 r3795449 4 4 use MailHawk\Api\Postal\Domains; 5 5 use function MailHawk\mailhawk_admin_page; 6 use function MailHawk\get_dns_instructions; 7 use function MailHawk\get_dns_record_names; 8 use function MailHawk\is_maybe_subdomain; 6 9 use function MailHawk\get_spf_record; 7 10 use function MailHawk\get_url_var; … … 47 50 <?php endif; ?> 48 51 52 <?php 53 54 // The same pair for every record: what goes in the Host or Name field, and the hostname it ends up as 55 $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name, $domain->name ); 56 $return_path_names = get_dns_record_names( $domain->return_path->return_path_domain, $domain->name ); 57 $spf_names = get_dns_record_names( '@', $domain->name ); 58 59 /** 60 * The full hostname in one pill, with the host inside it as a pill of its own. The host copies just the host, the 61 * rest of it copies the full hostname. 62 * 63 * @param array $names 64 */ 65 $name_fields = function ( $names ) use ( $domain ) { 66 67 // The domain itself has no part before the domain, so it is shown after the @ 68 $rest = '@' === $names['host'] ? strtolower( $domain->name ) : substr( $names['hostname'], strlen( $names['host'] ) ); 69 70 ?> 71 <div class="mailhawk-dns-name code" role="button" tabindex="0" 72 data-copy="<?php echo esc_attr( $names['hostname'] ); ?>" 73 title="<?php esc_attr_e( 'Copy the full hostname', 'mailhawk' ); ?>" 74 aria-label="<?php echo esc_attr( sprintf( 75 /* translators: %s: the full hostname of a DNS record */ 76 __( 'Copy the full hostname %s', 'mailhawk' ), $names['hostname'] ) ); ?>"> 77 <button type="button" class="mailhawk-dns-host" 78 data-copy="<?php echo esc_attr( $names['host'] ); ?>" 79 title="<?php esc_attr_e( 'Copy the host', 'mailhawk' ); ?>" 80 aria-label="<?php echo esc_attr( sprintf( 81 /* translators: %s: the host of a DNS record, relative to the domain */ 82 __( 'Copy the host %s', 'mailhawk' ), $names['host'] ) ); ?>"><?php echo esc_html( $names['host'] ); ?></button> 83 <span class="mailhawk-dns-rest<?php echo '@' === $names['host'] ? ' is-domain' : ''; ?>"><?php echo esc_html( $rest ); ?></span> 84 <span class="mailhawk-dns-copied" aria-live="polite"></span> 85 <span class="dashicons dashicons-admin-page" aria-hidden="true"></span> 86 </div> 87 <?php 88 }; 89 90 /** 91 * A pill like the name, without a part of its own: the whole of it copies the value 92 * 93 * @param string $value 94 * @param string $label what the value is, for a screen reader 95 */ 96 $copy_field = function ( $value, $label ) { 97 ?> 98 <div class="mailhawk-dns-name mailhawk-copy-value code" role="button" tabindex="0" 99 data-copy="<?php echo esc_attr( $value ); ?>" 100 data-copied="<?php esc_attr_e( 'Copied', 'mailhawk' ); ?>" 101 title="<?php echo esc_attr( $value ); ?>" 102 aria-label="<?php echo esc_attr( sprintf( 103 /* translators: 1: what is copied, like TTL or value, 2: the text that is copied */ 104 __( 'Copy the %1$s %2$s', 'mailhawk' ), $label, $value ) ); ?>"> 105 <span class="mailhawk-copy-text"><?php echo esc_html( $value ); ?></span> 106 <span class="mailhawk-dns-copied" aria-live="polite"></span> 107 <span class="dashicons dashicons-admin-page" aria-hidden="true"></span> 108 </div> 109 <?php 110 }; 111 112 $ttl_label = __( 'TTL', 'mailhawk' ); 113 $value_label = __( 'value', 'mailhawk' ); 114 115 ?> 116 <p class="description"> 117 <?php _e( 'Click the highlighted <b>host</b> to copy just that part, which is what most DNS providers ask for, or anywhere else on the name to copy the <b>full hostname</b>.', 'mailhawk' ); ?> 118 <?php if ( is_maybe_subdomain( $domain->name ) ) : ?> 119 <?php _e( 'If the records of a subdomain go in the DNS of its parent domain, add the subdomain to the host: for <code>academy.example.com</code> in the DNS of <code>example.com</code>, <code>postal-abc._domainkey</code> becomes <code>postal-abc._domainkey.academy</code>.', 'mailhawk' ); ?> 120 <?php endif; ?> 121 </p> 122 49 123 <table class="wp-list-table widefat fixed striped dns-list"> 50 124 <thead> … … 60 134 <tr> 61 135 <td><?php _e( '<code>TXT</code> (DKIM)' ) ?></td> 62 <td><input class="code" onfocus="this.select()" type="text" 63 value="<?php esc_attr_e( strtolower( $domain->dkim->dkim_record_name ) ); ?>" 64 readonly></td> 65 <td><input class="code" onfocus="this.select()" type="text" 66 value="3600" 67 readonly></td> 68 <td><input class="code" onfocus="this.select()" type="text" 69 value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>" 70 readonly></td> 136 <td><?php $name_fields( $dkim_names ); ?></td> 137 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 138 <td><?php $copy_field( (string) $domain->dkim->dkim_record, $value_label ); ?></td> 71 139 <td><?php echo $domain->dkim->dkim_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 72 140 </td> … … 74 142 <tr> 75 143 <td><?php _e( '<code>CNAME</code>' ) ?></td> 76 <td><input class="code" onfocus="this.select()" type="text" 77 value="<?php esc_attr_e( strtolower( $domain->return_path->return_path_domain ) ); ?>" 78 readonly></td> 79 <td><input class="code" onfocus="this.select()" type="text" 80 value="3600" 81 readonly></td> 82 <td><input class="code" onfocus="this.select()" type="text" 83 value="rp.mta01.mailhawk.io" 84 readonly></td> 144 <td><?php $name_fields( $return_path_names ); ?></td> 145 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 146 <td><?php $copy_field( 'rp.mta01.mailhawk.io', $value_label ); ?></td> 85 147 <td><?php echo $domain->return_path->return_path_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 86 148 </td> … … 88 150 <tr> 89 151 <td><?php _e( '<code>TXT</code> (SPF)' ) ?></td> 90 <td><input class="code" onfocus="this.select()" type="text" 91 value="@" 92 readonly></td> 93 <td><input class="code" onfocus="this.select()" type="text" 94 value="3600" 95 readonly></td> 152 <td><?php $name_fields( $spf_names ); ?></td> 153 <td><?php $copy_field( '3600', $ttl_label ); ?></td> 96 154 <td> 97 155 … … 109 167 endif; ?> 110 168 111 <input class="code" onfocus="this.select()" type="text" 112 value="<?php esc_attr_e( $spf_record ); ?>" 113 readonly></td> 169 <?php $copy_field( (string) $spf_record, $value_label ); ?></td> 114 170 <td><?php echo $domain->spf->spf_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?> 115 171 </td> … … 122 178 <a href="https://mailhawk.io/configure-dns/" 123 179 target="_blank"><?php _e( 'Learn how to configure your DNS', 'mailhawk' ); ?></a></p> 180 <p class="mailhawk-dns-handoff"> 181 <?php _e( 'Someone else manages your DNS, like an agency, a developer or an AI assistant?', 'mailhawk' ); ?> 182 <button type="button" class="button button-secondary mailhawk-copy-button" 183 data-copy="<?php echo esc_attr( get_dns_instructions( $domain ) ); ?>" 184 data-copied="<?php esc_attr_e( 'Instructions copied', 'mailhawk' ); ?>"> 185 <span class="dashicons dashicons-clipboard" aria-hidden="true"></span> 186 <span class="mailhawk-copy-button-label"><?php _e( 'Copy instructions', 'mailhawk' ); ?></span> 187 </button> 188 </p> 124 189 </div> 125 190 <p> -
mailhawk/trunk/admin/views/dns.php
r3788980 r3795449 3 3 // Todo get domains from API 4 4 use MailHawk\Api\Postal\Domains; 5 use function MailHawk\get_dns_record_names; 6 use function MailHawk\is_maybe_subdomain; 5 7 use function MailHawk\mailhawk_admin_page; 6 8 … … 58 60 readonly> 59 61 <h3><?php _e( 'DKIM Record' ); ?></h3> 60 <p class="description"><?php printf( __( 'You need to add a new TXT record with the name <code>%s</code> with the following content.', 'mailhawk' ), esc_html( strtolower( $domain->dkim->dkim_record_name ) ) ); ?></p> 62 <?php $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name, $domain->name ); ?> 63 <p class="description"><?php printf( 64 /* translators: 1: the record host, relative to the domain, 2: the full hostname */ 65 __( 'You need to add a new TXT record with the host <code>%1$s</code> (full hostname <code>%2$s</code>) with the following content.', 'mailhawk' ), 66 esc_html( $dkim_names['host'] ), 67 esc_html( $dkim_names['hostname'] ) 68 ); ?> 69 <?php if ( is_maybe_subdomain( $domain->name ) ) : ?> 70 <?php _e( 'If you add it in the DNS of a parent domain, add the subdomain to the host.', 'mailhawk' ); ?> 71 <?php endif; ?></p> 61 72 <input class="code" onfocus="this.select()" type="text" 62 73 value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>" -
mailhawk/trunk/admin/views/overview.php
r3794810 r3795449 148 148 <?php if ( $is_throttled ): ?> 149 149 <div class="mailhawk-content-box mailhawk-throttle-notice"> 150 <h2><?php _e( ' Email is being held back', 'mailhawk' ); ?></h2>150 <h2><?php _e( 'Your monthly email limit is reached', 'mailhawk' ); ?></h2> 151 151 <p> 152 152 <?php _e( "This site has used all of its emails for the month, so MailHawk is turning email away instead of sending it. The emails it turns away are marked as failed in the log.", 'mailhawk' ); ?> 153 153 <?php if ( $resets_at ): ?> 154 <?php printf( __( 'Sending starts again when the limit resets on %s (UTC), or as soon as the plan is upgraded.', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?> 154 <?php 155 /* translators: %s: date the monthly limit resets */ 156 printf( __( 'Sending starts again when the limit resets on %s (UTC), or as soon as the plan is upgraded.', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); 157 ?> 155 158 <?php else: ?> 156 159 <?php _e( 'Sending starts again when the limit resets, or as soon as the plan is upgraded.', 'mailhawk' ); ?> … … 234 237 </div> 235 238 <p> 236 <?php printf( 'Used: <b>%s</b> / %s', number_format_i18n( $usage ), number_format_i18n( $limit ) ); ?> 239 <?php 240 /* translators: 1: emails used this month, 2: monthly limit */ 241 printf( __( 'Used: %1$s / %2$s', 'mailhawk' ), '<b>' . number_format_i18n( $usage ) . '</b>', number_format_i18n( $limit ) ); 242 ?> 237 243 <?php if ( $resets_at ): ?> 238 <span class="usage-resets">· <?php printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?></span> 244 <span class="usage-resets">· <?php 245 /* translators: %s: date the monthly limit resets */ 246 printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); 247 ?></span> 239 248 <?php endif; ?> 240 249 </p> … … 246 255 <?php _e( 'Upgrade your plan so your email keeps going out.', 'mailhawk' ); ?> 247 256 <?php else: ?> 248 <b><?php printf( __( "You've used %s%% of this month's emails.", 'mailhawk' ), number_format_i18n( $percent_usage ) ); ?></b> 257 <b><?php 258 /* translators: %s: percentage of this month's emails used */ 259 printf( __( "You've used %s%% of this month's emails.", 'mailhawk' ), number_format_i18n( $percent_usage ) ); 260 ?></b> 249 261 <?php _e( 'Upgrade your plan before you run out.', 'mailhawk' ); ?> 250 262 <?php endif; ?> -
mailhawk/trunk/admin/views/suspended.php
r3793271 r3795449 47 47 <?php endif; ?> 48 48 <?php if ( $since ) : ?> 49 <p><?php printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); ?></p> 49 <p><?php 50 /* translators: %s: date and time the account was suspended */ 51 printf( esc_html__( 'Suspended since %s.', 'mailhawk' ), esc_html( $since ) ); 52 ?></p> 50 53 <?php endif; ?> 51 54 <?php if ( 'organization' === $suspension['scope'] ) : ?> 52 55 <p><?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?></p> 53 56 <?php endif; ?> 54 <p><?php _e( 'Your MailHawk account page says what to do next, for example to renew the subscription or to sendan incident report.', 'mailhawk' ); ?></p>57 <p><?php _e( 'Your MailHawk account page says what to do next, for example, renewing your subscription or sending an incident report.', 'mailhawk' ); ?></p> 55 58 <a class="button button-primary big-button mailhawk-cta" href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener"> 56 59 <span class="dashicons dashicons-external" aria-hidden="true"></span> -
mailhawk/trunk/admin/views/test.php
r2309454 r3795449 1 1 <?php 2 2 3 use MailHawk\Api\Postal\Domains; 4 use function MailHawk\get_array_var; 5 use function MailHawk\get_default_from_email_address; 6 use function MailHawk\get_test_email_faq; 7 use function MailHawk\get_test_from_addresses; 8 use function MailHawk\mailhawk_admin_page; 9 10 // The domains MailHawk knows, to suggest addresses to send from and to warn about ones that are not authenticated 11 $domains = Domains::query_all(); 12 $from = get_test_from_addresses( is_wp_error( $domains ) ? [] : $domains ); 13 14 // What was sent last stays, the default from address the first time 15 $from_address = get_array_var( $this->test_data, 'from' ) ?: strtolower( (string) get_default_from_email_address() ); 16 3 17 ?> 4 <div class="mailhawk-content-box domains ">18 <div class="mailhawk-content-box domains mailhawk-test-email"> 5 19 <p><b><?php _e( 'Send a test email:', 'mailhawk' ); ?></b></p> 6 20 7 21 <form method="post"> 8 22 <?php wp_nonce_field( 'send_test_email', '_mailhawk_nonce' ); ?> 23 <?php 24 25 // The default first, then the addresses on verified domains, then the rest 26 $groups = [ 'default' => [], 'verified' => [], 'unverified' => [] ]; 27 28 foreach ( $from['addresses'] as $address ) { 29 $groups[ $address['default'] ? 'default' : ( $address['authenticated'] ? 'verified' : 'unverified' ) ][] = $address['email']; 30 } 31 32 $suggested = in_array( $from_address, wp_list_pluck( $from['addresses'], 'email' ), true ); 33 34 ?> 9 35 <p> 10 <input type="text" name="subject" class="regular-text" placeholder="<?php esc_attr_e( 'Subject line...', 'mailhawk' ); ?>" value="<?php esc_attr_e( 'MailHawk is connected!', 'mailhawk' );?>" > 36 <label for="mailhawk-test-from-select"><?php _e( 'From', 'mailhawk' ); ?></label><br> 37 <select id="mailhawk-test-from-select" class="regular-text" hidden> 38 <?php foreach ( $groups['default'] as $email ) : ?> 39 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php 40 /* translators: %s: the default from email address */ 41 echo esc_html( sprintf( __( '%s (default)', 'mailhawk' ), $email ) ); 42 ?></option> 43 <?php endforeach; ?> 44 <?php if ( $groups['verified'] ) : ?> 45 <optgroup label="<?php esc_attr_e( 'Verified domains', 'mailhawk' ); ?>"> 46 <?php foreach ( $groups['verified'] as $email ) : ?> 47 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php echo esc_html( $email ); ?></option> 48 <?php endforeach; ?> 49 </optgroup> 50 <?php endif; ?> 51 <?php if ( $groups['unverified'] ) : ?> 52 <optgroup label="<?php esc_attr_e( 'Domains that are not verified', 'mailhawk' ); ?>"> 53 <?php foreach ( $groups['unverified'] as $email ) : ?> 54 <option value="<?php echo esc_attr( $email ); ?>" <?php selected( $email, $from_address ); ?>><?php echo esc_html( $email ); ?></option> 55 <?php endforeach; ?> 56 </optgroup> 57 <?php endif; ?> 58 <option value="" <?php selected( ! $suggested ); ?>><?php _e( 'Other address…', 'mailhawk' ); ?></option> 59 </select> 60 <?php // What is sent. Without JavaScript it is the field, with it the select fills it in and it is only shown for another address. ?> 61 <input type="email" name="from_address" id="mailhawk-test-from" class="regular-text" 62 aria-label="<?php esc_attr_e( 'From address', 'mailhawk' ); ?>" 63 placeholder="<?php esc_attr_e( 'name@yourdomain.com', 'mailhawk' ); ?>" 64 value="<?php echo esc_attr( $from_address ); ?>" 65 data-domains="<?php echo esc_attr( wp_json_encode( (object) $from['domains'] ) ); ?>"> 11 66 </p> 67 <div class="mailhawk-test-from-warning notice notice-warning inline" hidden 68 data-unverified="<?php 69 /* translators: %s: the domain of the from address */ 70 echo esc_attr__( 'The DNS records of %s are not all verified, so this email may go to spam or be refused.', 'mailhawk' ); 71 ?>" 72 data-unknown="<?php 73 /* translators: %s: the domain of the from address */ 74 echo esc_attr__( '%s is not one of your MailHawk domains, so this email can not be authenticated and may go to spam or be refused.', 'mailhawk' ); 75 ?>" 76 data-dns-url="<?php echo esc_attr( mailhawk_admin_page( [ 'view' => 'domains', 'domain' => '%s' ] ) ); ?>" 77 data-domains-url="<?php echo esc_attr( mailhawk_admin_page( [ 'view' => 'domains' ] ) ); ?>"> 78 <p> 79 <span class="mailhawk-test-from-warning-text"></span> 80 <a class="mailhawk-test-from-warning-link" href="#" 81 data-unverified="<?php esc_attr_e( 'Check the DNS records', 'mailhawk' ); ?>" 82 data-unknown="<?php esc_attr_e( 'Add the domain', 'mailhawk' ); ?>"></a> 83 </p> 84 </div> 12 85 <p> 13 <textarea name="content" class="regular-text" rows="4" placeholder="<?php esc_attr_e( 'Content...', 'mailhawk' ); ?>"><?php esc_html_e( 'Hello World!', 'mailhawk' ); ?></textarea> 14 </p> 15 <p> 16 <input type="email" name="to_address" class="regular-text" 86 <label for="mailhawk-test-to"><?php _e( 'To', 'mailhawk' ); ?></label><br> 87 <input type="email" name="to_address" id="mailhawk-test-to" class="regular-text" 17 88 value="<?php esc_attr_e( wp_get_current_user()->user_email ); ?>"> 18 89 <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Send Test' ); ?>"> 19 90 </p> 20 91 </form> 21 <p class="description"><?php _e( ' Send a test email to yourself to ensure MailHawk is properly connected and your deliverability is good.', 'mailhawk' ); ?></p>92 <p class="description"><?php _e( 'MailHawk sends a short email that confirms the site can send, with tips on what it means for where it lands. Send it to an address of your own at Gmail, Outlook or wherever your customers are, and see where it ends up.', 'mailhawk' ); ?></p> 22 93 23 94 </div> 95 <div class="mailhawk-content-box mailhawk-test-faq"> 96 <p><b><?php _e( 'Where did it land?', 'mailhawk' ); ?></b></p> 97 <?php foreach ( get_test_email_faq( true ) as $item ) : ?> 98 <details> 99 <summary><?php echo esc_html( $item['question'] ); ?></summary> 100 <p><?php echo wp_kses( $item['answer'], [ 'a' => [ 'href' => true ], 'b' => [], 'i' => [] ] ); ?></p> 101 </details> 102 <?php endforeach; ?> 103 </div> -
mailhawk/trunk/assets/css/admin.css
r3793271 r3795449 1108 1108 1109 1109 .not-sure-dns { 1110 border-color: #e1e5ea; 1110 1111 border-radius: 0 0 10px 10px; 1112 } 1113 1114 /* The test email */ 1115 .mailhawk-test-email label { 1116 font-weight: 600; 1117 } 1118 1119 .mailhawk-test-email .mailhawk-test-from-warning { 1120 max-width: 25em; 1121 margin: -4px 0 12px; 1122 } 1123 1124 .mailhawk-test-email .mailhawk-test-from-warning[hidden], 1125 .mailhawk-test-email [hidden] { 1126 display: none; 1127 } 1128 1129 /* The field for another address sits under the select */ 1130 .mailhawk-test-email #mailhawk-test-from-select + #mailhawk-test-from { 1131 display: block; 1132 margin-top: 8px; 1133 } 1134 1135 .mailhawk-test-email #mailhawk-test-from-select + #mailhawk-test-from[hidden] { 1136 display: none; 1137 } 1138 1139 /* Where the test email landed, one question open at a time is plenty */ 1140 .mailhawk-test-faq details { 1141 border-top: 1px solid #e1e5ea; 1142 } 1143 1144 .mailhawk-test-faq details:last-child { 1145 border-bottom: 1px solid #e1e5ea; 1146 } 1147 1148 .mailhawk-test-faq summary { 1149 padding: 12px 0; 1150 font-weight: 600; 1151 cursor: pointer; 1152 } 1153 1154 .mailhawk-test-faq details p { 1155 max-width: 50em; 1156 margin: 0 0 14px; 1157 color: #50575e; 1158 } 1159 1160 /* Handing the DNS records to someone else */ 1161 .not-sure-dns .mailhawk-dns-handoff { 1162 margin: 0; 1163 padding: 12px 0; 1164 border-top: 1px solid #e1e5ea; 1165 color: #50575e; 1166 } 1167 1168 /* The ID matches the rule that makes every button in a content box a block */ 1169 #wpbody .mailhawk-content-box .mailhawk-copy-button.button { 1170 display: inline-flex; 1171 align-items: center; 1172 gap: 4px; 1173 margin-left: 6px; 1174 vertical-align: middle; 1175 } 1176 1177 .mailhawk-copy-button .dashicons { 1178 font-size: 16px; 1179 width: 16px; 1180 height: 16px; 1181 } 1182 1183 .mailhawk-copy-button.button.copied, 1184 .mailhawk-copy-button.button.copied:hover, 1185 .mailhawk-copy-button.button.copied:focus { 1186 border-color: #2ba123; 1187 background: #e3f5e8; 1188 color: #146c2e; 1189 } 1190 1191 /* The DNS table and the help under it are one card, so the table only rounds its top corners */ 1192 .mailhawk-view-content .wp-list-table.dns-list { 1193 border-bottom-left-radius: 0; 1194 border-bottom-right-radius: 0; 1111 1195 } 1112 1196 … … 1627 1711 } 1628 1712 } 1713 1714 /* DNS record names: the full hostname as a pill, with the host as a pill inside it */ 1715 .mailhawk-dns-name { 1716 display: flex; 1717 align-items: center; 1718 flex-wrap: wrap; 1719 gap: 0; 1720 box-sizing: border-box; 1721 width: 100%; 1722 min-height: 40px; 1723 padding: 4px 10px 4px 4px; 1724 border: 1px solid #cfd6de; 1725 border-radius: 6px; 1726 background: #f6f7f7; 1727 color: #1d2327; 1728 font-family: Consolas, Monaco, monospace; 1729 font-size: 14px; 1730 line-height: 1.4; 1731 cursor: copy; 1732 word-break: break-all; 1733 transition: border-color .15s, background-color .15s; 1734 } 1735 1736 .mailhawk-dns-name:hover, 1737 .mailhawk-dns-name:focus-visible { 1738 border-color: #2271b1; 1739 background: #f0f6fc; 1740 outline: none; 1741 } 1742 1743 .mailhawk-dns-name .mailhawk-dns-host { 1744 padding: 3px 7px; 1745 border: 1px solid #2271b1; 1746 border-radius: 4px; 1747 background: #fff; 1748 color: #2271b1; 1749 font: inherit; 1750 cursor: copy; 1751 transition: background-color .15s, color .15s; 1752 } 1753 1754 .mailhawk-dns-name .mailhawk-dns-host:hover, 1755 .mailhawk-dns-name .mailhawk-dns-host:focus-visible { 1756 background: #2271b1; 1757 color: #fff; 1758 outline: none; 1759 } 1760 1761 .mailhawk-dns-name .mailhawk-dns-rest { 1762 padding-left: 1px; 1763 color: #50575e; 1764 } 1765 1766 /* The domain itself, shown after the @ */ 1767 .mailhawk-dns-name .mailhawk-dns-rest.is-domain { 1768 padding-left: 8px; 1769 } 1770 1771 .mailhawk-dns-name .dashicons { 1772 margin-left: auto; 1773 padding-left: 8px; 1774 color: #8c8f94; 1775 font-size: 16px; 1776 line-height: 20px; 1777 } 1778 1779 .mailhawk-dns-name:hover .dashicons { 1780 color: #2271b1; 1781 } 1782 1783 .mailhawk-dns-name .mailhawk-dns-copied { 1784 margin-left: auto; 1785 padding-left: 8px; 1786 color: #146c2e; 1787 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 1788 font-size: 12px; 1789 white-space: nowrap; 1790 } 1791 1792 .mailhawk-dns-name .mailhawk-dns-copied:empty { 1793 display: none; 1794 } 1795 1796 /* Copied: the icon makes way for what was copied, and the part that was copied shows it */ 1797 .mailhawk-dns-name.copied-host .dashicons, 1798 .mailhawk-dns-name.copied-hostname .dashicons { 1799 display: none; 1800 } 1801 1802 .mailhawk-dns-name.copied-hostname { 1803 border-color: #2ba123; 1804 background: #e3f5e8; 1805 } 1806 1807 .mailhawk-dns-name.copied-host .mailhawk-dns-host { 1808 border-color: #2ba123; 1809 background: #2ba123; 1810 color: #fff; 1811 } 1812 1813 /* The TTL and value: one line, a long value is cut short on screen but copied whole */ 1814 .mailhawk-dns-name.mailhawk-copy-value { 1815 flex-wrap: nowrap; 1816 padding-left: 10px; 1817 text-align: left; 1818 word-break: normal; 1819 } 1820 1821 .mailhawk-copy-value .mailhawk-copy-text { 1822 flex: 1 1 auto; 1823 min-width: 0; 1824 overflow: hidden; 1825 text-overflow: ellipsis; 1826 white-space: nowrap; 1827 } -
mailhawk/trunk/assets/js/admin.js
r3788980 r3795449 1 (function ($, connect, review, groundhogg) { 1 (function ($, connect, review, groundhogg, dns) { 2 3 // Copy text, with a fallback for wp-admin served without HTTPS, where the clipboard API is not there 4 function copyText(text) { 5 if (navigator.clipboard && window.isSecureContext) { 6 return navigator.clipboard.writeText(text); 7 } 8 9 var $field = $('<textarea readonly>').val(text).css({position: 'fixed', top: 0, left: 0, opacity: 0}).appendTo('body'); 10 $field[0].select(); 11 12 var copied = document.execCommand('copy'); 13 $field.remove(); 14 15 return copied ? $.Deferred().resolve().promise() : $.Deferred().reject().promise(); 16 } 17 18 // The names on the DNS screen: the host inside copies just the host, the rest of the pill the full hostname. The 19 // TTL and value pills copy what they show. 20 $(document).on('click', '.mailhawk-dns-name, .mailhawk-dns-host', function (e) { 21 e.stopPropagation(); 22 23 var $target = $(this); 24 var $name = $target.closest('.mailhawk-dns-name'); 25 var isHost = $target.is('.mailhawk-dns-host'); 26 27 $.when(copyText($target.attr('data-copy'))).done(function () { 28 // A value says it in its own words, a name says which part 29 $name.find('.mailhawk-dns-copied').text($name.attr('data-copied') || (isHost ? dns.host_copied : dns.hostname_copied)); 30 $name.toggleClass('copied-host', isHost).toggleClass('copied-hostname', !isHost); 31 32 clearTimeout($name.data('copiedTimer')); 33 $name.data('copiedTimer', setTimeout(function () { 34 $name.removeClass('copied-host copied-hostname').find('.mailhawk-dns-copied').text(''); 35 }, 1500)); 36 }); 37 }); 38 39 // A button that copies a block of text, like the DNS instructions, and says so on itself for a moment 40 $(document).on('click', '.mailhawk-copy-button', function () { 41 var $button = $(this); 42 var $label = $button.find('.mailhawk-copy-button-label'); 43 44 if (!$button.data('label')) { 45 $button.data('label', $label.text()); 46 } 47 48 $.when(copyText($button.attr('data-copy'))).done(function () { 49 $label.text($button.attr('data-copied')); 50 $button.addClass('copied'); 51 52 clearTimeout($button.data('copiedTimer')); 53 $button.data('copiedTimer', setTimeout(function () { 54 $label.text($button.data('label')); 55 $button.removeClass('copied'); 56 }, 2000)); 57 }); 58 }); 59 60 // The test email: say so when the address it is sent from is on a domain that is not verified, or not MailHawk's 61 $(function () { 62 var $from = $('#mailhawk-test-from'); 63 var $warning = $('.mailhawk-test-from-warning'); 64 65 if (!$from.length || !$warning.length) { 66 return; 67 } 68 69 var domains = $from.data('domains') || {}; 70 var $link = $warning.find('.mailhawk-test-from-warning-link'); 71 var $select = $('#mailhawk-test-from-select'); 72 73 // The select picks a suggested address, its last choice opens the field for any other 74 function pick() { 75 var value = $select.val(); 76 77 $from.prop('hidden', value !== ''); 78 79 if (value !== '') { 80 $from.val(value); 81 } else if ($select.find('option[value="' + $from.val() + '"]').length) { 82 // Coming from a suggestion, start the other address empty 83 $from.val('').trigger('focus'); 84 } 85 86 check(); 87 } 88 89 $select.prop('hidden', false).on('change', pick); 90 91 function check() { 92 var email = $.trim($from.val()).toLowerCase(); 93 var at = email.lastIndexOf('@'); 94 var domain = at > 0 ? email.slice(at + 1) : ''; 95 96 // Nothing to say until there is an address to say it about, or when its domain is verified 97 if (!domain || domain.indexOf('.') === -1 || domains[domain] === true) { 98 $warning.prop('hidden', true); 99 return; 100 } 101 102 var known = Object.prototype.hasOwnProperty.call(domains, domain); 103 var kind = known ? 'unverified' : 'unknown'; 104 105 $warning.find('.mailhawk-test-from-warning-text').text($warning.attr('data-' + kind).replace('%s', domain)); 106 $link.text($link.attr('data-' + kind)).attr('href', known 107 ? $warning.attr('data-dns-url').replace('%s', encodeURIComponent(domain)) 108 : $warning.attr('data-domains-url')); 109 110 $warning.prop('hidden', false); 111 } 112 113 $from.on('input change', check); 114 pick(); 115 }); 116 117 // The pill is not a button element, it holds one, so Enter and Space are handled here 118 $(document).on('keydown', '.mailhawk-dns-name', function (e) { 119 if (e.target === this && (e.key === 'Enter' || e.key === ' ')) { 120 e.preventDefault(); 121 $(this).trigger('click'); 122 } 123 }); 2 124 3 125 // The suggestions to look at Groundhogg, on the overview and the recipients screen. Learn more opens WordPress' own … … 133 255 134 256 135 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg );257 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg, MailHawkDns); -
mailhawk/trunk/includes/abilities/ability.php
r3793271 r3795449 134 134 135 135 if ( mailhawk_is_suspended() ) { 136 return $this->error( 'suspended', __( 'The MailHawk account is suspended , mailhawk/get-status says why and where to go next.', 'mailhawk' ), 409 );136 return $this->error( 'suspended', __( 'The MailHawk account is suspended. Use mailhawk/get-status to see why and what to do next.', 'mailhawk' ), 409 ); 137 137 } 138 138 -
mailhawk/trunk/includes/abilities/email/send-test-email.php
r3788980 r3795449 6 6 use MailHawk\Plugin; 7 7 use WP_Error; 8 use function MailHawk\get_test_email; 9 use function MailHawk\set_next_alt_body; 8 10 9 11 if ( ! defined( 'ABSPATH' ) ) { … … 83 85 } 84 86 85 $site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );86 $ subject = sprintf( __( 'MailHawk test email from %s', 'mailhawk' ), $site);87 $ message = sprintf( __( "This is a test email from %s, sent through MailHawk to check that email is delivered.\n\nYou can ignore it.", 'mailhawk' ), $site );87 // The same email the Test screen sends 88 $email = get_test_email( $to ); 89 $subject = $email['subject']; 88 90 89 91 $table = Plugin::instance()->log->get_table_name(); … … 98 100 add_action( 'wp_mail_failed', $catch ); 99 101 102 $alt_body = set_next_alt_body( $email['text'] ); 103 100 104 // Straight to MailHawk's mailer, which is what wp_mail() is while the site is connected. A test that went through 101 105 // whatever else is hooked in would not say anything about MailHawk. 102 106 try { 103 $sent = mailhawk_mail( $to, $subject, $ message);107 $sent = mailhawk_mail( $to, $subject, $email['html'], [ 'Content-Type: text/html' ] ); 104 108 } catch ( \Exception $e ) { 105 109 $sent = false; … … 107 111 } finally { 108 112 remove_action( 'wp_mail_failed', $catch ); 113 remove_action( 'phpmailer_init', $alt_body, 99 ); 109 114 } 110 115 -
mailhawk/trunk/includes/abilities/formatter.php
r3788980 r3795449 4 4 5 5 use MailHawk\DB\Recipients; 6 use function MailHawk\get_dns_record_names; 6 7 use function MailHawk\get_email_status_pretty_name; 7 8 use function MailHawk\get_spf_record; … … 187 188 } 188 189 190 // What goes in the Host field, relative to the domain, and the full hostname, as the DNS screen shows them 191 $dkim_names = get_dns_record_names( $domain->dkim->dkim_record_name ?? '', (string) $domain->name ); 192 $return_path_names = get_dns_record_names( $domain->return_path->return_path_domain ?? '', (string) $domain->name ); 193 $spf_names = get_dns_record_names( '@', (string) $domain->name ); 194 189 195 return [ 190 196 'records' => [ … … 192 198 'purpose' => 'dkim', 193 199 'type' => 'TXT', 200 'host' => $dkim_names['host'], 201 'hostname' => $dkim_names['hostname'], 194 202 'name' => strtolower( (string) ( $domain->dkim->dkim_record_name ?? '' ) ), 195 203 'value' => (string) ( $domain->dkim->dkim_record ?? '' ), … … 201 209 'purpose' => 'return_path', 202 210 'type' => 'CNAME', 211 'host' => $return_path_names['host'], 212 'hostname' => $return_path_names['hostname'], 203 213 'name' => strtolower( (string) ( $domain->return_path->return_path_domain ?? '' ) ), 204 214 'value' => self::RETURN_PATH_TARGET, … … 210 220 'purpose' => 'spf', 211 221 'type' => 'TXT', 222 'host' => $spf_names['host'], 223 'hostname' => $spf_names['hostname'], 212 224 'name' => '@', 213 225 'value' => $spf_record, … … 231 243 'records' => [ 232 244 'type' => 'array', 233 'description' => __( 'The records to add at the DNS host of the domain, as they are shown on the MailHawk DNS screen. A name of @ is the domain itself.', 'mailhawk' ),245 'description' => __( 'The records to add at the DNS host of the domain, as they are shown on the MailHawk DNS screen. host is what goes in the Host or Name field, relative to the domain, and @ is the domain itself. hostname is the full name. For a subdomain whose records are added in the DNS of its parent domain, the subdomain is added to host. name is kept for compatibility: the DKIM name is relative and the return path name is full.', 'mailhawk' ), 234 246 'items' => [ 235 247 'type' => 'object', … … 243 255 'enum' => [ 'TXT', 'CNAME' ], 244 256 ], 257 'host' => [ 'type' => 'string' ], 258 'hostname' => [ 'type' => 'string' ], 245 259 'name' => [ 'type' => 'string' ], 246 260 'value' => [ 'type' => 'string' ], … … 249 263 'verified' => [ 'type' => 'boolean' ], 250 264 ], 251 'required' => [ 'purpose', 'type', ' name', 'value', 'ttl', 'status', 'verified' ],265 'required' => [ 'purpose', 'type', 'host', 'hostname', 'name', 'value', 'ttl', 'status', 'verified' ], 252 266 ], 253 267 ], -
mailhawk/trunk/includes/abilities/status/get-status.php
r3793271 r3795449 238 238 239 239 $message = $details['reason'] 240 /* translators: %s: the reason MailHawk gave for suspending the account */ 240 241 ? sprintf( __( 'The MailHawk account is suspended: %s', 'mailhawk' ), $details['reason'] ) 241 242 : __( 'The MailHawk account is suspended.', 'mailhawk' ); 242 243 244 /* translators: %s: URL of the MailHawk account page */ 243 245 $issues[] = [ 'code' => 'suspended', 'message' => $message . ' ' . sprintf( __( 'What to do next is on the MailHawk account page, %s, where a person has to sign in.', 'mailhawk' ), mailhawk_account_url() ) ]; 244 246 } … … 257 259 258 260 if ( $usage && $usage['percent'] >= 90 ) { 261 /* translators: 1: percentage of the monthly allowance used, 2: emails left */ 259 262 $issues[] = [ 'code' => 'usage_high', 'message' => sprintf( __( '%1$s%% of the monthly sending allowance is used, %2$s emails are left.', 'mailhawk' ), $usage['percent'], number_format_i18n( $usage['remaining'] ) ) ]; 260 263 } -
mailhawk/trunk/includes/api/postal/messages.php
r3788980 r3795449 62 62 63 63 if ( ! $api_key ) { 64 return new \WP_Error( 'mailhawk_rejected', 'Invalid API key.');64 return new \WP_Error( 'mailhawk_rejected', __( 'Invalid API key.', 'mailhawk' ) ); 65 65 } 66 66 -
mailhawk/trunk/includes/api/postal/postal.php
r3793271 r3795449 53 53 54 54 if ( ! $api_key ){ 55 return new \WP_Error( 'error', 'Invalid API key.');55 return new \WP_Error( 'error', __( 'Invalid API key.', 'mailhawk' ) ); 56 56 } 57 57 … … 93 93 return new \WP_Error( 94 94 'mailhawk_rate_limited', 95 __( 'MailHawk is slowing down email from this site, usually because the monthly sending limit of the plan is reached. Upgrade the plan, or wait for the limit to reset.', 'mailhawk' ),95 __( "MailHawk is slowing down email from this site, usually because the plan's monthly sending limit has been reached. Upgrade the plan, or wait for the limit to reset.", 'mailhawk' ), 96 96 [ 'retry_after' => $retry_after ? $retry_after : 5 * MINUTE_IN_SECONDS ] 97 97 ); 98 98 } 99 99 100 $result = new \WP_Error( 'error', 'Unable to complete request.');100 $result = new \WP_Error( 'error', __( 'Unable to complete request.', 'mailhawk' ) ); 101 101 102 102 // Postal … … 108 108 break; 109 109 case 'parameter-error': 110 $result = new \WP_Error( 'parameter-error', $json->data->message ?? 'Invalid request.');110 $result = new \WP_Error( 'parameter-error', $json->data->message ?? __( 'Invalid request.', 'mailhawk' ) ); 111 111 break; 112 112 case 'error': … … 120 120 $message = __( 'MailHawk has suspended this account.', 'mailhawk' ); 121 121 } else { 122 $message = sprintf( 'MailHawk returned an error: %s', $code ); 122 /* translators: %s: error code returned by MailHawk */ 123 $message = sprintf( __( 'MailHawk returned an error: %s', 'mailhawk' ), $code ); 123 124 } 124 125 -
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; -
mailhawk/trunk/includes/quarantine.php
r3788980 r3795449 104 104 ] ); 105 105 106 // Someone must have take care of it already106 // Someone must have taken care of it already 107 107 if ( $count_quarantined === 0 ) { 108 108 return; -
mailhawk/trunk/includes/site-health.php
r3794810 r3795449 132 132 'recommended', 133 133 __( 'MailHawk is not connected', 'mailhawk' ), 134 __( 'Email from this site is not going out through MailHawk, so it is sent the way WordPress does it by default, and itmay not reach the inbox.', 'mailhawk' ),134 __( 'Email from this site is not going out through MailHawk, so it is sent with the default WordPress mailer and may not reach the inbox.', 'mailhawk' ), 135 135 $this->link( mailhawk_admin_page(), __( 'Connect MailHawk', 'mailhawk' ) ) 136 136 ); … … 143 143 $description = [ __( 'The MailHawk service is not accepting email from this site, so none of it is going out.', 'mailhawk' ) ]; 144 144 145 // Older MTA builds do not say why 145 // Older MTA builds do not say why. The reason may not end in a period, so it gets its own. 146 146 $description[] = $suspension['reason'] 147 ? sprintf( __( 'The reason given: %s', 'mailhawk' ), $suspension['reason'] ) 147 /* translators: %s: the reason MailHawk gave for suspending the account */ 148 ? sprintf( __( 'Reason: %s.', 'mailhawk' ), rtrim( $suspension['reason'], '. ' ) ) 148 149 : __( 'This is usually a problem with the account or the subscription.', 'mailhawk' ); 149 150 150 151 if ( $since ) { 152 /* translators: %s: date and time the account was suspended */ 151 153 $description[] = sprintf( __( 'Suspended since %s.', 'mailhawk' ), $since ); 152 154 } … … 200 202 'critical', 201 203 __( 'MailHawk is turning email away', 'mailhawk' ), 202 __( "This site has used all of its emails for the month, so MailHawk is not sending what it is asked to. The emails it turned away are marked as failed in the log. Sending starts again when the limit resets, or as soon as the plan is upgraded.", 'mailhawk' ),204 __( "This site has used all of its emails for the month, so MailHawk is not sending new email. The emails it turned away are marked as failed in the log. Sending starts again when the limit resets, or as soon as the plan is upgraded.", 'mailhawk' ), 203 205 $this->upgrade_link() . $this->link( mailhawk_admin_page( [ 'view' => 'log', 'status' => 'failed' ] ), __( 'See the failed emails', 'mailhawk' ) ) 204 206 ); … … 309 311 'critical', 310 312 __( 'This site used all of its emails for the month', 'mailhawk' ), 313 /* translators: 1: emails sent this month, 2: emails in the plan */ 311 314 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan. MailHawk turns email away until the limit resets.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ), 312 315 $this->upgrade_link() … … 319 322 'recommended', 320 323 __( 'This site is close to the limit of its plan', 'mailhawk' ), 324 /* translators: 1: emails sent this month, 2: emails in the plan */ 321 325 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan this month. When they are used up, MailHawk turns email away until the limit resets.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ), 322 326 $this->upgrade_link() … … 328 332 'good', 329 333 __( 'This site has emails left this month', 'mailhawk' ), 334 /* translators: 1: emails sent this month, 2: emails in the plan */ 330 335 sprintf( __( 'This site sent %1$s of the %2$s emails in its plan this month.', 'mailhawk' ), number_format_i18n( $used ), number_format_i18n( $limit ) ) 331 336 ); -
mailhawk/trunk/mailhawk.php
r3794810 r3795449 5 5 * Plugin URI: https://mailhawk.io 6 6 * Description: Send better email that will reach the inbox with MailHawk. 7 * Version: 1.5. 17 * Version: 1.5.2 8 8 * Requires at least: 6.9 9 9 * Requires PHP: 7.4 … … 26 26 if ( ! defined( 'ABSPATH' ) ) exit; 27 27 28 define( 'MAILHAWK_VERSION', '1.5. 1' );29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5. 0' );28 define( 'MAILHAWK_VERSION', '1.5.2' ); 29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.1' ); 30 30 define( 'MAILHAWK_LICENSE_SERVER_URL', 'https://mailhawk.io' ); 31 31 -
mailhawk/trunk/readme.txt
r3794810 r3795449 5 5 Requires at least: 6.9 6 6 Tested up to: 7.1 7 Stable tag: 1.5. 17 Stable tag: 1.5.2 8 8 Requires PHP: 7.4 9 9 License: GPLv3 or later … … 167 167 168 168 == Changelog == 169 170 = 1.5.2 (2026-11-10) = 171 * ADDED The test email can be sent from any address. A dropdown suggests the default from address and the same name at each of your domains, and a warning says when the domain of the address you pick is not verified, or not one of your MailHawk domains. 172 * IMPROVED The test email is now a branded email that says what it means for where it landed: the inbox, Gmail's Updates or Promotions tabs, Outlook's Other folder, or spam. The Test screen has the same tips, and what to do when it did not arrive at all. 173 * ADDED A "Copy instructions" button on the DNS screen copies everything someone else needs to add the records, like an agency, a developer or an AI assistant: every record with its host, full hostname, value and TTL, what to do with an SPF record that is already there, and where to verify. 174 * IMPROVED Record names on the DNS screen copy with one click: the highlighted host copies just the host, anywhere else copies the full hostname. The screen also explains what to enter when the records of a subdomain go in the DNS of its parent domain, and the host of the return path no longer includes your domain, so DNS providers that add it themselves don't end up with it twice. 175 * TWEAKED Clearer wording on the overview, in Site Health, and on the suspended screen and notice. When the monthly limit is reached, the notice on the overview is titled "Your monthly email limit is reached". 176 * FIXED Some error messages and the usage line on the overview could not be translated. 177 * DEV The `mailhawk/get-domain-dns` ability returns the `host` and the full `hostname` of each record. 178 * DEV New filter `mailhawk/second_level_suffixes` lists the suffixes like co.uk that domains are registered under, so the DNS screen does not take example.co.uk for a subdomain. 169 179 170 180 = 1.5.1 (2026-11-10) = -
mailhawk/trunk/uninstall.php
r3793271 r3795449 7 7 * @package MailHawk 8 8 * @subpackage Uninstall 9 * @copyright Copyright (c) 2015, Pippin Williamson10 * @license http ://opensource.org/licenses/gpl-2.0.php GNU Public License11 * @since 1. 4.39 * @copyright Copyright (c) MailHawk Inc. 10 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 or later 11 * @since 1.0.0 12 12 */ 13 13 … … 15 15 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit; 16 16 17 // Load WPGH file.17 // Load the plugin so its tables can be dropped. 18 18 include_once __DIR__ . '/mailhawk.php'; 19 19
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)