Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.56% covered (warning)
75.56%
34 / 45
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Email_Service
75.56% covered (warning)
75.56%
34 / 45
66.67% covered (warning)
66.67%
4 / 6
18.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 api_send_auth_email
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 send_email_request
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 resend_auth_email
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 generate_auth_code
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mask_email_address
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Class used to define Email Service.
4 *
5 * @package automattic/jetpack-account-protection
6 */
7
8namespace Automattic\Jetpack\Account_Protection;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Jetpack_Options;
13
14/**
15 * Class Email_Service
16 */
17class Email_Service {
18    /**
19     * Connection manager dependency.
20     *
21     * @var Connection_Manager
22     */
23    private $connection_manager;
24
25    /**
26     * Constructor for dependency injection.
27     *
28     * @param Connection_Manager|null $connection_manager Connection manager dependency.
29     */
30    public function __construct(
31        ?Connection_Manager $connection_manager = null
32    ) {
33        $this->connection_manager = $connection_manager ?? new Connection_Manager();
34    }
35
36    /**
37     * Send the email using the API.
38     *
39     * @param int    $user_id The user ID.
40     * @param string $auth_code The authentication code.
41     *
42     * @return true|\WP_Error True if the email was sent successfully, \WP_Error otherwise.
43     */
44    public function api_send_auth_email( int $user_id, string $auth_code ) {
45        $blog_id = Jetpack_Options::get_option( 'id' );
46
47        if ( ! $blog_id || ! $this->connection_manager->is_connected() ) {
48            return new \WP_Error( 'jetpack_connection_error', __( 'Jetpack is not connected. Please connect and try again.', 'jetpack-account-protection' ) );
49        }
50
51        $body = array(
52            'user_id' => $user_id,
53            'code'    => $auth_code,
54        );
55
56        $response = $this->send_email_request( (int) $blog_id, $body );
57        if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
58            return new \WP_Error( 'email_send_error', __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
59        }
60
61        $response_code = wp_remote_retrieve_response_code( $response );
62        $body          = json_decode( wp_remote_retrieve_body( $response ), true );
63        if ( 200 !== $response_code ) {
64            return new \WP_Error( $body['code'] ?? 'email_send_error', $body['message'] ?? __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
65        }
66
67        if ( empty( $body['email_send_success'] ) ) {
68            return new \WP_Error( 'email_send_error', __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
69        }
70
71        return true;
72    }
73
74    /**
75     * Dependency decoupling for the static call to the client.
76     *
77     * @param int   $blog_id Blog ID.
78     * @param array $body The request body.
79     * @return array|\WP_Error Response data or error.
80     */
81    protected function send_email_request( int $blog_id, array $body ) {
82        return Client::wpcom_json_api_request_as_blog(
83            sprintf( '/sites/%d/jetpack-protect-send-verification-code', $blog_id ),
84            '2',
85            array(
86                'method' => 'POST',
87            ),
88            $body,
89            'wpcom'
90        );
91    }
92
93    /**
94     * Resend email attempts.
95     *
96     * @param int    $user_id The user ID.
97     * @param array  $transient_data The transient data.
98     * @param string $token The token.
99     *
100     * @return true|\WP_Error True if the email was resent successfully, \WP_Error otherwise.
101     */
102    public function resend_auth_email( int $user_id, array $transient_data, string $token ) {
103        if ( $transient_data['requests'] >= Config::PASSWORD_DETECTION_EMAIL_REQUEST_LIMIT ) {
104            return new \WP_Error( 'email_request_limit_exceeded', __( 'Email request limit exceeded. Please try again later.', 'jetpack-account-protection' ) );
105        }
106
107        $auth_code                   = $this->generate_auth_code();
108        $transient_data['auth_code'] = $auth_code;
109
110        $resend = $this->api_send_auth_email( $user_id, $auth_code );
111        if ( is_wp_error( $resend ) ) {
112            return $resend;
113        }
114
115        ++$transient_data['requests'];
116
117        if ( ! set_transient( Config::PREFIX . "_{$token}", $transient_data, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION ) ) {
118            return new \WP_Error( 'transient_set_error', __( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ) );
119        }
120
121        return true;
122    }
123
124    /**
125     * Generate an auth code.
126     *
127     * @return string The generated auth code.
128     */
129    public function generate_auth_code(): string {
130        return (string) wp_rand( 100000, 999999 );
131    }
132
133    /**
134     * Mask an email address like d*****@g*****.com.
135     *
136     * @param string $email The email address to mask.
137     *
138     * @return string The masked email address.
139     */
140    public function mask_email_address( string $email ): string {
141        $parts        = explode( '@', $email );
142        $name         = substr( $parts[0], 0, 1 ) . str_repeat( '*', strlen( $parts[0] ) - 1 );
143        $domain_parts = explode( '.', $parts[1] );
144        $domain       = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 );
145
146        // Join all domain parts except the first one with dots
147        $tld = implode( '.', array_slice( $domain_parts, 1 ) );
148
149        return "{$name}@{$domain}.{$tld}";
150    }
151}