Plugin Directory

Changeset 3795449


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

Update to version 1.5.2 from GitHub

Location:
mailhawk
Files:
2 added
40 edited
1 copied

Legend:

Unmodified
Added
Removed
  • mailhawk/tags/1.5.2/admin/admin.php

    r3794810 r3795449  
    1515use function MailHawk\dismiss_notice;
    1616use function MailHawk\get_array_var;
     17use function MailHawk\get_test_email;
     18use function MailHawk\set_next_alt_body;
    1719use function MailHawk\get_dismissible_notices;
    1820use function MailHawk\get_hostname;
    … …  
    579581                }
    580582
    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;
    588588
    589589                if ( ! $to ) {
    … …  
    593593                add_action( 'wp_mail_failed', [ $this, 'maybe_email_failed' ] );
    594594
    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 );
    598610
    599611                if ( $success ) {
    … …  
    11001112                wp_localize_script( 'mailhawk-admin', 'MailHawkConnect', [
    11011113                        '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' ),
    11021120                ] );
    11031121
    … …  
    14031421            <p>
    14041422                                <?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                                        ?>
    14061427                                <?php else : ?>
    14071428                                        <?php _e( '<b>Attention:</b> It looks like your account has been suspended. To continue sending email you must reactivate your account.', 'mailhawk' ); ?>
    14081429                                <?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; ?>
    14151430            </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; ?>
    14161444            <p>
    14171445                <a href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener"
    … …  
    15491577                ?>
    15501578        <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>
    15521588        </div>
    15531589                <?php
  • mailhawk/tags/1.5.2/admin/views/dns-single.php

    r3788980 r3795449  
    44use MailHawk\Api\Postal\Domains;
    55use function MailHawk\mailhawk_admin_page;
     6use function MailHawk\get_dns_instructions;
     7use function MailHawk\get_dns_record_names;
     8use function MailHawk\is_maybe_subdomain;
    69use function MailHawk\get_spf_record;
    710use function MailHawk\get_url_var;
    … …  
    4750        <?php endif; ?>
    4851
     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
    49123    <table class="wp-list-table widefat fixed striped dns-list">
    50124        <thead>
    … …  
    60134        <tr>
    61135            <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>
    71139            <td><?php echo $domain->dkim->dkim_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    72140            </td>
    … …  
    74142        <tr>
    75143            <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>
    85147            <td><?php echo $domain->return_path->return_path_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    86148            </td>
    … …  
    88150        <tr>
    89151            <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>
    96154            <td>
    97155
    … …  
    109167                        endif; ?>
    110168
    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>
    114170            <td><?php echo $domain->spf->spf_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    115171            </td>
    … …  
    122178            <a href="https://mailhawk.io/configure-dns/"
    123179               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>
    124189    </div>
    125190    <p>
  • mailhawk/tags/1.5.2/admin/views/dns.php

    r3788980 r3795449  
    33// Todo get domains from API
    44use MailHawk\Api\Postal\Domains;
     5use function MailHawk\get_dns_record_names;
     6use function MailHawk\is_maybe_subdomain;
    57use function MailHawk\mailhawk_admin_page;
    68
    … …  
    5860                   readonly>
    5961            <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>
    6172            <input class="code" onfocus="this.select()" type="text"
    6273                   value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>"
  • mailhawk/tags/1.5.2/admin/views/overview.php

    r3794810 r3795449  
    148148<?php if ( $is_throttled ): ?>
    149149    <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>
    151151        <p>
    152152                        <?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' ); ?>
    153153                        <?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                                ?>
    155158                        <?php else: ?>
    156159                                <?php _e( 'Sending starts again when the limit resets, or as soon as the plan is upgraded.', 'mailhawk' ); ?>
    … …  
    234237    </div>
    235238    <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                ?>
    237243                <?php if ( $resets_at ): ?>
    238             <span class="usage-resets">&middot; <?php printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?></span>
     244            <span class="usage-resets">&middot; <?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>
    239248                <?php endif; ?>
    240249    </p>
    … …  
    246255                                        <?php _e( 'Upgrade your plan so your email keeps going out.', 'mailhawk' ); ?>
    247256                                <?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>
    249261                                        <?php _e( 'Upgrade your plan before you run out.', 'mailhawk' ); ?>
    250262                                <?php endif; ?>
  • mailhawk/tags/1.5.2/admin/views/suspended.php

    r3793271 r3795449  
    4747                <?php endif; ?>
    4848                <?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>
    5053                <?php endif; ?>
    5154                <?php if ( 'organization' === $suspension['scope'] ) : ?>
    5255            <p><?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?></p>
    5356                <?php endif; ?>
    54         <p><?php _e( 'Your MailHawk account page says what to do next, for example to renew the subscription or to send an 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>
    5558        <a class="button button-primary big-button mailhawk-cta" href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener">
    5659            <span class="dashicons dashicons-external" aria-hidden="true"></span>
  • mailhawk/tags/1.5.2/admin/views/test.php

    r2309454 r3795449  
    11<?php
    22
     3use MailHawk\Api\Postal\Domains;
     4use function MailHawk\get_array_var;
     5use function MailHawk\get_default_from_email_address;
     6use function MailHawk\get_test_email_faq;
     7use function MailHawk\get_test_from_addresses;
     8use 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
    317?>
    4 <div class="mailhawk-content-box domains">
     18<div class="mailhawk-content-box domains mailhawk-test-email">
    519        <p><b><?php _e( 'Send a test email:', 'mailhawk' ); ?></b></p>
    620
    721    <form method="post">
    822            <?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                ?>
    935        <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&hellip;', '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'] ) ); ?>">
    1166        </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>
    1285        <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"
    1788                   value="<?php esc_attr_e( wp_get_current_user()->user_email ); ?>">
    1889            <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Send Test' ); ?>">
    1990        </p>
    2091    </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>
    2293
    2394</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  
    11081108
    11091109.not-sure-dns {
     1110    border-color: #e1e5ea;
    11101111    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;
    11111195}
    11121196
    … …  
    16271711    }
    16281712}
     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    });
    2124
    3125    // The suggestions to look at Groundhogg, on the overview and the recipients screen. Learn more opens WordPress' own
    … …  
    133255
    134256
    135 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg);
     257})(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg, MailHawkDns);
  • mailhawk/tags/1.5.2/includes/abilities/ability.php

    r3793271 r3795449  
    134134
    135135                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 );
    137137                }
    138138
  • mailhawk/tags/1.5.2/includes/abilities/email/send-test-email.php

    r3788980 r3795449  
    66use MailHawk\Plugin;
    77use WP_Error;
     8use function MailHawk\get_test_email;
     9use function MailHawk\set_next_alt_body;
    810
    911if ( ! defined( 'ABSPATH' ) ) {
    … …  
    8385                }
    8486
    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'];
    8890
    8991                $table = Plugin::instance()->log->get_table_name();
    … …  
    98100                add_action( 'wp_mail_failed', $catch );
    99101
     102                $alt_body = set_next_alt_body( $email['text'] );
     103
    100104                // Straight to MailHawk's mailer, which is what wp_mail() is while the site is connected. A test that went through
    101105                // whatever else is hooked in would not say anything about MailHawk.
    102106                try {
    103                         $sent = mailhawk_mail( $to, $subject, $message );
     107                        $sent = mailhawk_mail( $to, $subject, $email['html'], [ 'Content-Type: text/html' ] );
    104108                } catch ( \Exception $e ) {
    105109                        $sent    = false;
    … …  
    107111                } finally {
    108112                        remove_action( 'wp_mail_failed', $catch );
     113                        remove_action( 'phpmailer_init', $alt_body, 99 );
    109114                }
    110115
  • mailhawk/tags/1.5.2/includes/abilities/formatter.php

    r3788980 r3795449  
    44
    55use MailHawk\DB\Recipients;
     6use function MailHawk\get_dns_record_names;
    67use function MailHawk\get_email_status_pretty_name;
    78use function MailHawk\get_spf_record;
    … …  
    187188                }
    188189
     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
    189195                return [
    190196                        'records'             => [
    … …  
    192198                                        'purpose'  => 'dkim',
    193199                                        'type'     => 'TXT',
     200                                        'host'     => $dkim_names['host'],
     201                                        'hostname' => $dkim_names['hostname'],
    194202                                        'name'     => strtolower( (string) ( $domain->dkim->dkim_record_name ?? '' ) ),
    195203                                        'value'    => (string) ( $domain->dkim->dkim_record ?? '' ),
    … …  
    201209                                        'purpose'  => 'return_path',
    202210                                        'type'     => 'CNAME',
     211                                        'host'     => $return_path_names['host'],
     212                                        'hostname' => $return_path_names['hostname'],
    203213                                        'name'     => strtolower( (string) ( $domain->return_path->return_path_domain ?? '' ) ),
    204214                                        'value'    => self::RETURN_PATH_TARGET,
    … …  
    210220                                        'purpose'  => 'spf',
    211221                                        'type'     => 'TXT',
     222                                        'host'     => $spf_names['host'],
     223                                        'hostname' => $spf_names['hostname'],
    212224                                        'name'     => '@',
    213225                                        'value'    => $spf_record,
    … …  
    231243                                'records'             => [
    232244                                        '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' ),
    234246                                        'items'       => [
    235247                                                'type'       => 'object',
    … …  
    243255                                                                'enum' => [ 'TXT', 'CNAME' ],
    244256                                                        ],
     257                                                        'host'     => [ 'type' => 'string' ],
     258                                                        'hostname' => [ 'type' => 'string' ],
    245259                                                        'name'     => [ 'type' => 'string' ],
    246260                                                        'value'    => [ 'type' => 'string' ],
    … …  
    249263                                                        'verified' => [ 'type' => 'boolean' ],
    250264                                                ],
    251                                                 'required'   => [ 'purpose', 'type', 'name', 'value', 'ttl', 'status', 'verified' ],
     265                                                'required'   => [ 'purpose', 'type', 'host', 'hostname', 'name', 'value', 'ttl', 'status', 'verified' ],
    252266                                        ],
    253267                                ],
  • mailhawk/tags/1.5.2/includes/abilities/status/get-status.php

    r3793271 r3795449  
    238238
    239239                        $message = $details['reason']
     240                                /* translators: %s: the reason MailHawk gave for suspending the account */
    240241                                ? sprintf( __( 'The MailHawk account is suspended: %s', 'mailhawk' ), $details['reason'] )
    241242                                : __( 'The MailHawk account is suspended.', 'mailhawk' );
    242243
     244                        /* translators: %s: URL of the MailHawk account page */
    243245                        $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() ) ];
    244246                }
    … …  
    257259
    258260                if ( $usage && $usage['percent'] >= 90 ) {
     261                        /* translators: 1: percentage of the monthly allowance used, 2: emails left */
    259262                        $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'] ) ) ];
    260263                }
  • mailhawk/tags/1.5.2/includes/api/postal/messages.php

    r3788980 r3795449  
    6262
    6363                        if ( ! $api_key ) {
    64                                 return new \WP_Error( 'mailhawk_rejected', 'Invalid API key.' );
     64                                return new \WP_Error( 'mailhawk_rejected', __( 'Invalid API key.', 'mailhawk' ) );
    6565                        }
    6666
  • mailhawk/tags/1.5.2/includes/api/postal/postal.php

    r3793271 r3795449  
    5353
    5454                        if ( ! $api_key ){
    55                                 return new \WP_Error( 'error', 'Invalid API key.' );
     55                                return new \WP_Error( 'error', __( 'Invalid API key.', 'mailhawk' ) );
    5656                        }
    5757
    … …  
    9393                        return new \WP_Error(
    9494                                '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' ),
    9696                                [ 'retry_after' => $retry_after ? $retry_after : 5 * MINUTE_IN_SECONDS ]
    9797                        );
    9898                }
    9999
    100                 $result = new \WP_Error( 'error', 'Unable to complete request.' );
     100                $result = new \WP_Error( 'error', __( 'Unable to complete request.', 'mailhawk' ) );
    101101
    102102                // Postal
    … …  
    108108                                        break;
    109109                                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' ) );
    111111                                        break;
    112112                                case 'error':
    … …  
    120120                                                $message = __( 'MailHawk has suspended this account.', 'mailhawk' );
    121121                                        } 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 );
    123124                                        }
    124125
  • mailhawk/tags/1.5.2/includes/functions.php

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

    r3788980 r3795449  
    104104                ] );
    105105
    106                 // Someone must have take care of it already
     106                // Someone must have taken care of it already
    107107                if ( $count_quarantined === 0 ) {
    108108                        return;
  • mailhawk/tags/1.5.2/includes/site-health.php

    r3794810 r3795449  
    132132                                'recommended',
    133133                                __( '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 it may 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' ),
    135135                                $this->link( mailhawk_admin_page(), __( 'Connect MailHawk', 'mailhawk' ) )
    136136                        );
    … …  
    143143                        $description = [ __( 'The MailHawk service is not accepting email from this site, so none of it is going out.', 'mailhawk' ) ];
    144144
    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.
    146146                        $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'], '. ' ) )
    148149                                : __( 'This is usually a problem with the account or the subscription.', 'mailhawk' );
    149150
    150151                        if ( $since ) {
     152                                /* translators: %s: date and time the account was suspended */
    151153                                $description[] = sprintf( __( 'Suspended since %s.', 'mailhawk' ), $since );
    152154                        }
    … …  
    200202                                'critical',
    201203                                __( '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' ),
    203205                                $this->upgrade_link() . $this->link( mailhawk_admin_page( [ 'view' => 'log', 'status' => 'failed' ] ), __( 'See the failed emails', 'mailhawk' ) )
    204206                        );
    … …  
    309311                                'critical',
    310312                                __( 'This site used all of its emails for the month', 'mailhawk' ),
     313                                /* translators: 1: emails sent this month, 2: emails in the plan */
    311314                                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 ) ),
    312315                                $this->upgrade_link()
    … …  
    319322                                'recommended',
    320323                                __( 'This site is close to the limit of its plan', 'mailhawk' ),
     324                                /* translators: 1: emails sent this month, 2: emails in the plan */
    321325                                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 ) ),
    322326                                $this->upgrade_link()
    … …  
    328332                        'good',
    329333                        __( 'This site has emails left this month', 'mailhawk' ),
     334                        /* translators: 1: emails sent this month, 2: emails in the plan */
    330335                        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 ) )
    331336                );
  • mailhawk/tags/1.5.2/mailhawk.php

    r3794810 r3795449  
    55 * Plugin URI: https://mailhawk.io
    66 * Description: Send better email that will reach the inbox with MailHawk.
    7  * Version: 1.5.1
     7 * Version: 1.5.2
    88 * Requires at least: 6.9
    99 * Requires PHP: 7.4
    … …  
    2626if ( ! defined( 'ABSPATH' ) ) exit;
    2727
    28 define( 'MAILHAWK_VERSION', '1.5.1' );
    29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.0' );
     28define( 'MAILHAWK_VERSION', '1.5.2' );
     29define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.1' );
    3030define( 'MAILHAWK_LICENSE_SERVER_URL', 'https://mailhawk.io' );
    3131
  • mailhawk/tags/1.5.2/readme.txt

    r3794810 r3795449  
    55Requires at least: 6.9
    66Tested up to: 7.1
    7 Stable tag: 1.5.1
     7Stable tag: 1.5.2
    88Requires PHP: 7.4
    99License: GPLv3 or later
    … …  
    167167
    168168== 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.
    169179
    170180= 1.5.1 (2026-11-10) =
  • mailhawk/tags/1.5.2/uninstall.php

    r3793271 r3795449  
    77 * @package     MailHawk
    88 * @subpackage  Uninstall
    9  * @copyright   Copyright (c) 2015, Pippin Williamson
    10  * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
    11  * @since       1.4.3
     9 * @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
    1212 */
    1313
    … …  
    1515if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit;
    1616
    17 // Load WPGH file.
     17// Load the plugin so its tables can be dropped.
    1818include_once __DIR__ . '/mailhawk.php';
    1919
  • mailhawk/trunk/admin/admin.php

    r3794810 r3795449  
    1515use function MailHawk\dismiss_notice;
    1616use function MailHawk\get_array_var;
     17use function MailHawk\get_test_email;
     18use function MailHawk\set_next_alt_body;
    1719use function MailHawk\get_dismissible_notices;
    1820use function MailHawk\get_hostname;
    … …  
    579581                }
    580582
    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;
    588588
    589589                if ( ! $to ) {
    … …  
    593593                add_action( 'wp_mail_failed', [ $this, 'maybe_email_failed' ] );
    594594
    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 );
    598610
    599611                if ( $success ) {
    … …  
    11001112                wp_localize_script( 'mailhawk-admin', 'MailHawkConnect', [
    11011113                        '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' ),
    11021120                ] );
    11031121
    … …  
    14031421            <p>
    14041422                                <?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                                        ?>
    14061427                                <?php else : ?>
    14071428                                        <?php _e( '<b>Attention:</b> It looks like your account has been suspended. To continue sending email you must reactivate your account.', 'mailhawk' ); ?>
    14081429                                <?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; ?>
    14151430            </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; ?>
    14161444            <p>
    14171445                <a href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener"
    … …  
    15491577                ?>
    15501578        <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>
    15521588        </div>
    15531589                <?php
  • mailhawk/trunk/admin/views/dns-single.php

    r3788980 r3795449  
    44use MailHawk\Api\Postal\Domains;
    55use function MailHawk\mailhawk_admin_page;
     6use function MailHawk\get_dns_instructions;
     7use function MailHawk\get_dns_record_names;
     8use function MailHawk\is_maybe_subdomain;
    69use function MailHawk\get_spf_record;
    710use function MailHawk\get_url_var;
    … …  
    4750        <?php endif; ?>
    4851
     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
    49123    <table class="wp-list-table widefat fixed striped dns-list">
    50124        <thead>
    … …  
    60134        <tr>
    61135            <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>
    71139            <td><?php echo $domain->dkim->dkim_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    72140            </td>
    … …  
    74142        <tr>
    75143            <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>
    85147            <td><?php echo $domain->return_path->return_path_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    86148            </td>
    … …  
    88150        <tr>
    89151            <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>
    96154            <td>
    97155
    … …  
    109167                        endif; ?>
    110168
    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>
    114170            <td><?php echo $domain->spf->spf_status === 'OK' ? "<span class='tag yes'>" . __( 'Verified', 'mailhawk' ) . "</span>" : "<span class='tag no'>" . __( 'Unverified' ) . "</span>"; ?>
    115171            </td>
    … …  
    122178            <a href="https://mailhawk.io/configure-dns/"
    123179               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>
    124189    </div>
    125190    <p>
  • mailhawk/trunk/admin/views/dns.php

    r3788980 r3795449  
    33// Todo get domains from API
    44use MailHawk\Api\Postal\Domains;
     5use function MailHawk\get_dns_record_names;
     6use function MailHawk\is_maybe_subdomain;
    57use function MailHawk\mailhawk_admin_page;
    68
    … …  
    5860                   readonly>
    5961            <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>
    6172            <input class="code" onfocus="this.select()" type="text"
    6273                   value="<?php esc_attr_e( $domain->dkim->dkim_record ); ?>"
  • mailhawk/trunk/admin/views/overview.php

    r3794810 r3795449  
    148148<?php if ( $is_throttled ): ?>
    149149    <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>
    151151        <p>
    152152                        <?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' ); ?>
    153153                        <?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                                ?>
    155158                        <?php else: ?>
    156159                                <?php _e( 'Sending starts again when the limit resets, or as soon as the plan is upgraded.', 'mailhawk' ); ?>
    … …  
    234237    </div>
    235238    <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                ?>
    237243                <?php if ( $resets_at ): ?>
    238             <span class="usage-resets">&middot; <?php printf( __( 'Resets on %s (UTC)', 'mailhawk' ), esc_html( date_i18n( get_option( 'date_format' ), $resets_at, true ) ) ); ?></span>
     244            <span class="usage-resets">&middot; <?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>
    239248                <?php endif; ?>
    240249    </p>
    … …  
    246255                                        <?php _e( 'Upgrade your plan so your email keeps going out.', 'mailhawk' ); ?>
    247256                                <?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>
    249261                                        <?php _e( 'Upgrade your plan before you run out.', 'mailhawk' ); ?>
    250262                                <?php endif; ?>
  • mailhawk/trunk/admin/views/suspended.php

    r3793271 r3795449  
    4747                <?php endif; ?>
    4848                <?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>
    5053                <?php endif; ?>
    5154                <?php if ( 'organization' === $suspension['scope'] ) : ?>
    5255            <p><?php _e( 'The whole MailHawk account is suspended, so none of its sites can send email.', 'mailhawk' ); ?></p>
    5356                <?php endif; ?>
    54         <p><?php _e( 'Your MailHawk account page says what to do next, for example to renew the subscription or to send an 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>
    5558        <a class="button button-primary big-button mailhawk-cta" href="<?php echo esc_url( mailhawk_account_url() ); ?>" target="_blank" rel="noopener">
    5659            <span class="dashicons dashicons-external" aria-hidden="true"></span>
  • mailhawk/trunk/admin/views/test.php

    r2309454 r3795449  
    11<?php
    22
     3use MailHawk\Api\Postal\Domains;
     4use function MailHawk\get_array_var;
     5use function MailHawk\get_default_from_email_address;
     6use function MailHawk\get_test_email_faq;
     7use function MailHawk\get_test_from_addresses;
     8use 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
    317?>
    4 <div class="mailhawk-content-box domains">
     18<div class="mailhawk-content-box domains mailhawk-test-email">
    519        <p><b><?php _e( 'Send a test email:', 'mailhawk' ); ?></b></p>
    620
    721    <form method="post">
    822            <?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                ?>
    935        <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&hellip;', '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'] ) ); ?>">
    1166        </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>
    1285        <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"
    1788                   value="<?php esc_attr_e( wp_get_current_user()->user_email ); ?>">
    1889            <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Send Test' ); ?>">
    1990        </p>
    2091    </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>
    2293
    2394</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  
    11081108
    11091109.not-sure-dns {
     1110    border-color: #e1e5ea;
    11101111    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;
    11111195}
    11121196
    … …  
    16271711    }
    16281712}
     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    });
    2124
    3125    // The suggestions to look at Groundhogg, on the overview and the recipients screen. Learn more opens WordPress' own
    … …  
    133255
    134256
    135 })(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg);
     257})(jQuery, MailHawkConnect, MailHawkReview, MailHawkGroundhogg, MailHawkDns);
  • mailhawk/trunk/includes/abilities/ability.php

    r3793271 r3795449  
    134134
    135135                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 );
    137137                }
    138138
  • mailhawk/trunk/includes/abilities/email/send-test-email.php

    r3788980 r3795449  
    66use MailHawk\Plugin;
    77use WP_Error;
     8use function MailHawk\get_test_email;
     9use function MailHawk\set_next_alt_body;
    810
    911if ( ! defined( 'ABSPATH' ) ) {
    … …  
    8385                }
    8486
    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'];
    8890
    8991                $table = Plugin::instance()->log->get_table_name();
    … …  
    98100                add_action( 'wp_mail_failed', $catch );
    99101
     102                $alt_body = set_next_alt_body( $email['text'] );
     103
    100104                // Straight to MailHawk's mailer, which is what wp_mail() is while the site is connected. A test that went through
    101105                // whatever else is hooked in would not say anything about MailHawk.
    102106                try {
    103                         $sent = mailhawk_mail( $to, $subject, $message );
     107                        $sent = mailhawk_mail( $to, $subject, $email['html'], [ 'Content-Type: text/html' ] );
    104108                } catch ( \Exception $e ) {
    105109                        $sent    = false;
    … …  
    107111                } finally {
    108112                        remove_action( 'wp_mail_failed', $catch );
     113                        remove_action( 'phpmailer_init', $alt_body, 99 );
    109114                }
    110115
  • mailhawk/trunk/includes/abilities/formatter.php

    r3788980 r3795449  
    44
    55use MailHawk\DB\Recipients;
     6use function MailHawk\get_dns_record_names;
    67use function MailHawk\get_email_status_pretty_name;
    78use function MailHawk\get_spf_record;
    … …  
    187188                }
    188189
     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
    189195                return [
    190196                        'records'             => [
    … …  
    192198                                        'purpose'  => 'dkim',
    193199                                        'type'     => 'TXT',
     200                                        'host'     => $dkim_names['host'],
     201                                        'hostname' => $dkim_names['hostname'],
    194202                                        'name'     => strtolower( (string) ( $domain->dkim->dkim_record_name ?? '' ) ),
    195203                                        'value'    => (string) ( $domain->dkim->dkim_record ?? '' ),
    … …  
    201209                                        'purpose'  => 'return_path',
    202210                                        'type'     => 'CNAME',
     211                                        'host'     => $return_path_names['host'],
     212                                        'hostname' => $return_path_names['hostname'],
    203213                                        'name'     => strtolower( (string) ( $domain->return_path->return_path_domain ?? '' ) ),
    204214                                        'value'    => self::RETURN_PATH_TARGET,
    … …  
    210220                                        'purpose'  => 'spf',
    211221                                        'type'     => 'TXT',
     222                                        'host'     => $spf_names['host'],
     223                                        'hostname' => $spf_names['hostname'],
    212224                                        'name'     => '@',
    213225                                        'value'    => $spf_record,
    … …  
    231243                                'records'             => [
    232244                                        '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' ),
    234246                                        'items'       => [
    235247                                                'type'       => 'object',
    … …  
    243255                                                                'enum' => [ 'TXT', 'CNAME' ],
    244256                                                        ],
     257                                                        'host'     => [ 'type' => 'string' ],
     258                                                        'hostname' => [ 'type' => 'string' ],
    245259                                                        'name'     => [ 'type' => 'string' ],
    246260                                                        'value'    => [ 'type' => 'string' ],
    … …  
    249263                                                        'verified' => [ 'type' => 'boolean' ],
    250264                                                ],
    251                                                 'required'   => [ 'purpose', 'type', 'name', 'value', 'ttl', 'status', 'verified' ],
     265                                                'required'   => [ 'purpose', 'type', 'host', 'hostname', 'name', 'value', 'ttl', 'status', 'verified' ],
    252266                                        ],
    253267                                ],
  • mailhawk/trunk/includes/abilities/status/get-status.php

    r3793271 r3795449  
    238238
    239239                        $message = $details['reason']
     240                                /* translators: %s: the reason MailHawk gave for suspending the account */
    240241                                ? sprintf( __( 'The MailHawk account is suspended: %s', 'mailhawk' ), $details['reason'] )
    241242                                : __( 'The MailHawk account is suspended.', 'mailhawk' );
    242243
     244                        /* translators: %s: URL of the MailHawk account page */
    243245                        $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() ) ];
    244246                }
    … …  
    257259
    258260                if ( $usage && $usage['percent'] >= 90 ) {
     261                        /* translators: 1: percentage of the monthly allowance used, 2: emails left */
    259262                        $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'] ) ) ];
    260263                }
  • mailhawk/trunk/includes/api/postal/messages.php

    r3788980 r3795449  
    6262
    6363                        if ( ! $api_key ) {
    64                                 return new \WP_Error( 'mailhawk_rejected', 'Invalid API key.' );
     64                                return new \WP_Error( 'mailhawk_rejected', __( 'Invalid API key.', 'mailhawk' ) );
    6565                        }
    6666
  • mailhawk/trunk/includes/api/postal/postal.php

    r3793271 r3795449  
    5353
    5454                        if ( ! $api_key ){
    55                                 return new \WP_Error( 'error', 'Invalid API key.' );
     55                                return new \WP_Error( 'error', __( 'Invalid API key.', 'mailhawk' ) );
    5656                        }
    5757
    … …  
    9393                        return new \WP_Error(
    9494                                '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' ),
    9696                                [ 'retry_after' => $retry_after ? $retry_after : 5 * MINUTE_IN_SECONDS ]
    9797                        );
    9898                }
    9999
    100                 $result = new \WP_Error( 'error', 'Unable to complete request.' );
     100                $result = new \WP_Error( 'error', __( 'Unable to complete request.', 'mailhawk' ) );
    101101
    102102                // Postal
    … …  
    108108                                        break;
    109109                                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' ) );
    111111                                        break;
    112112                                case 'error':
    … …  
    120120                                                $message = __( 'MailHawk has suspended this account.', 'mailhawk' );
    121121                                        } 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 );
    123124                                        }
    124125
  • mailhawk/trunk/includes/functions.php

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

    r3788980 r3795449  
    104104                ] );
    105105
    106                 // Someone must have take care of it already
     106                // Someone must have taken care of it already
    107107                if ( $count_quarantined === 0 ) {
    108108                        return;
  • mailhawk/trunk/includes/site-health.php

    r3794810 r3795449  
    132132                                'recommended',
    133133                                __( '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 it may 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' ),
    135135                                $this->link( mailhawk_admin_page(), __( 'Connect MailHawk', 'mailhawk' ) )
    136136                        );
    … …  
    143143                        $description = [ __( 'The MailHawk service is not accepting email from this site, so none of it is going out.', 'mailhawk' ) ];
    144144
    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.
    146146                        $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'], '. ' ) )
    148149                                : __( 'This is usually a problem with the account or the subscription.', 'mailhawk' );
    149150
    150151                        if ( $since ) {
     152                                /* translators: %s: date and time the account was suspended */
    151153                                $description[] = sprintf( __( 'Suspended since %s.', 'mailhawk' ), $since );
    152154                        }
    … …  
    200202                                'critical',
    201203                                __( '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' ),
    203205                                $this->upgrade_link() . $this->link( mailhawk_admin_page( [ 'view' => 'log', 'status' => 'failed' ] ), __( 'See the failed emails', 'mailhawk' ) )
    204206                        );
    … …  
    309311                                'critical',
    310312                                __( 'This site used all of its emails for the month', 'mailhawk' ),
     313                                /* translators: 1: emails sent this month, 2: emails in the plan */
    311314                                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 ) ),
    312315                                $this->upgrade_link()
    … …  
    319322                                'recommended',
    320323                                __( 'This site is close to the limit of its plan', 'mailhawk' ),
     324                                /* translators: 1: emails sent this month, 2: emails in the plan */
    321325                                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 ) ),
    322326                                $this->upgrade_link()
    … …  
    328332                        'good',
    329333                        __( 'This site has emails left this month', 'mailhawk' ),
     334                        /* translators: 1: emails sent this month, 2: emails in the plan */
    330335                        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 ) )
    331336                );
  • mailhawk/trunk/mailhawk.php

    r3794810 r3795449  
    55 * Plugin URI: https://mailhawk.io
    66 * Description: Send better email that will reach the inbox with MailHawk.
    7  * Version: 1.5.1
     7 * Version: 1.5.2
    88 * Requires at least: 6.9
    99 * Requires PHP: 7.4
    … …  
    2626if ( ! defined( 'ABSPATH' ) ) exit;
    2727
    28 define( 'MAILHAWK_VERSION', '1.5.1' );
    29 define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.0' );
     28define( 'MAILHAWK_VERSION', '1.5.2' );
     29define( 'MAILHAWK_PREVIOUS_STABLE_VERSION', '1.5.1' );
    3030define( 'MAILHAWK_LICENSE_SERVER_URL', 'https://mailhawk.io' );
    3131
  • mailhawk/trunk/readme.txt

    r3794810 r3795449  
    55Requires at least: 6.9
    66Tested up to: 7.1
    7 Stable tag: 1.5.1
     7Stable tag: 1.5.2
    88Requires PHP: 7.4
    99License: GPLv3 or later
    … …  
    167167
    168168== 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.
    169179
    170180= 1.5.1 (2026-11-10) =
  • mailhawk/trunk/uninstall.php

    r3793271 r3795449  
    77 * @package     MailHawk
    88 * @subpackage  Uninstall
    9  * @copyright   Copyright (c) 2015, Pippin Williamson
    10  * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
    11  * @since       1.4.3
     9 * @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
    1212 */
    1313
    … …  
    1515if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit;
    1616
    17 // Load WPGH file.
     17// Load the plugin so its tables can be dropped.
    1818include_once __DIR__ . '/mailhawk.php';
    1919
Note: See TracChangeset for help on using the changeset viewer.