Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.04% covered (warning)
59.04%
49 / 83
20.00% covered (danger)
20.00%
2 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Newsletter
59.04% covered (warning)
59.04%
49 / 83
20.00% covered (danger)
20.00%
2 / 10
77.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_description
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 handle_ses_bounce
56.25% covered (warning)
56.25%
9 / 16
0.00% covered (danger)
0.00%
0 / 1
9.01
 handle_ses_complaint
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
4.25
 handle_unsubscribe_email
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 setup_test
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 verify_test
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
4.20
 delete_test_data
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Functionality for the Newsletter plugin to mark users as bounced and unsubscribe users who complain.
4 *
5 * @see https://wordpress.org/plugins/newsletter
6 *
7 * @link       https://BrianHenry.ie
8 * @since      1.0.0
9 *
10 * @package brianhenryie/bh-wp-aws-ses-bounce-handler
11 */
12
13namespace BrianHenryIE\AWS_SES_Bounce_Handler\API\Integrations;
14
15use BrianHenryIE\AWS_SES_Bounce_Handler\Admin\Bounce_Handler_Test;
16
17use BrianHenryIE\AWS_SES_Bounce_Handler\API\SES_Bounce_Handler_Integration_Interface;
18use Psr\Log\LoggerAwareTrait;
19use Psr\Log\LoggerInterface;
20use stdClass;
21use TNP;
22
23/**
24 * `handle_ses_bounce` => Mark the user bounced.
25 * `handle_ses_complaint` => Unsubscribe the user.
26 */
27class Newsletter implements SES_Bounce_Handler_Integration_Interface {
28
29    use LoggerAwareTrait;
30
31    /**
32     * Constructor.
33     *
34     * @param LoggerInterface $logger A PSR logger.
35     */
36    public function __construct( LoggerInterface $logger ) {
37        $this->setLogger( $logger );
38    }
39
40    /**
41     * Links to the Newsletter subscribers page if the plugin is active.
42     *
43     * @return string
44     */
45    public function get_description(): string {
46
47        $html = 'Marks users as bounced and unsubscribes complaints';
48
49        return $html;
50    }
51
52    /**
53     * No initialization needed.
54     */
55    public function init(): void {
56    }
57
58    /**
59     * Are the plugin classes we'll use present?
60     *
61     * @return bool
62     */
63    public function is_enabled(): bool {
64        return class_exists( \Newsletter::class ) && class_exists( \TNP::class );
65    }
66
67    /**
68     * Mark email addresses as bounced in Newsletter plugin.
69     *
70     * @hooked handle_ses_bounce
71     *
72     * @param string   $email_address    The email address that has bounced.
73     * @param stdClass $bounced_recipient Parent object with emailAddress, status, action, diagnosticCode.
74     * @param stdClass $message           Parent object of complete notification.
75     *
76     * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
77     * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
78     */
79    public function handle_ses_bounce( string $email_address, stdClass $bounced_recipient, stdClass $message ): void {
80
81        if ( ! $this->is_enabled() ) {
82            return;
83        }
84
85        if ( ! defined( 'NEWSLETTER_USERS_TABLE' ) ) {
86            return;
87        }
88
89        $newsletter = \Newsletter::instance();
90        $user       = $newsletter->get_user( $email_address );
91
92        if ( empty( $user ) ) {
93            $this->logger->debug( "No matching TNP user found for Email address {$email_address}." );
94            return;
95        }
96
97        if ( 'B' === $user->status ) {
98            $this->logger->debug( "`TNP_User:{$user->id}` already has bounced status." );
99            return;
100        }
101
102        $user = $newsletter->set_user_status( $user, 'B' );
103
104        if ( 'B' === $user->status ) {
105            $this->logger->info( "`TNP_User:{$user->id}` status set to bounced." );
106        } else {
107            $this->logger->error( "Error setting `TNP_User:{$user->id}` status to bounced." );
108        }
109    }
110
111    /**
112     * Unsubscribe user from future emails.
113     *
114     * @hooked handle_ses_complaint
115     *
116     * @param string   $email_address     The email address that has bounced.
117     * @param stdClass $complained_recipient Parent object with emailAddress, status, action, diagnosticCode.
118     * @param stdClass $message           Parent object of complete notification.
119     */
120    public function handle_ses_complaint( string $email_address, stdClass $complained_recipient, stdClass $message ): void {
121
122        if ( ! $this->is_enabled() ) {
123            return;
124        }
125
126        $params          = array();
127        $params['email'] = $email_address;
128
129        $newsletter = \Newsletter::instance();
130        $user       = $newsletter->get_user( $email_address );
131
132        if ( empty( $user ) ) {
133            $this->logger->debug( "No matching TNP user found for Email address {$email_address}" );
134            return;
135        }
136
137        $log_unsubscribe_action = function( $subscriber ) {
138            $this->logger->info( "`tnp_user:{$subscriber->id}` unsubscribed after complaint." );
139        };
140
141        /**
142         * Hook into the Newsletter plugin's own unsubscribe confirmed action.
143         *
144         * @see TNP::unsubscribe()
145         */
146        add_action( 'newsletter_unsubscribed', $log_unsubscribe_action );
147
148        /** Returns WP_Error|void. */
149        $result = TNP::unsubscribe( $params );
150
151        if ( ! empty( $result ) ) {
152            $this->logger->error( "Failed to unsubscribe {$email_address} after complaint: " . $result->get_error_message() );
153        }
154
155        remove_action( 'newsletter_unsubscribed', $log_unsubscribe_action );
156
157        // TODO: Associate the complaint with the particular newsletter sent.
158    }
159
160    /**
161     * Unsubscribe user from future emails.
162     *
163     * @hooked handle_unsubscribe_email
164     *
165     * @param string   $email_address     The email address that has bounced.
166     * @param stdClass $complained_recipient Parent object with emailAddress, status, action, diagnosticCode.
167     * @param stdClass $message           Parent object of complete notification.
168     */
169    public function handle_unsubscribe_email( string $email_address, stdClass $complained_recipient, stdClass $message ): void {
170
171        if ( ! $this->is_enabled() ) {
172            return;
173        }
174
175        $params          = array();
176        $params['email'] = $email_address;
177
178        $newsletter = \Newsletter::instance();
179        $user       = $newsletter->get_user( $email_address );
180
181        if ( empty( $user ) ) {
182            $this->logger->debug( "No matching TNP user found for Email address {$email_address}" );
183            return;
184        }
185
186        $log_unsubscribe_action = function( $subscriber ) {
187            $this->logger->info( "`tnp_user:{$subscriber->id}` unsubscribed after unsubscribe request." );
188        };
189
190        /**
191         * Hook into the Newsletter plugin's own unsubscribe confirmed action.
192         *
193         * @see TNP::unsubscribe()
194         */
195        add_action( 'newsletter_unsubscribed', $log_unsubscribe_action );
196
197        /** Returns WP_Error|void. */
198        $result = TNP::unsubscribe( $params );
199
200        if ( ! empty( $result ) ) {
201            $this->logger->error( "Failed to unsubscribe {$email_address} after unsubscribe request: " . $result->get_error_message() );
202        }
203
204        remove_action( 'newsletter_unsubscribed', $log_unsubscribe_action );
205
206        // TODO: Associate the response with the particular newsletter sent.
207    }
208
209    /**
210     * Create a subscriber with the appropriate email address.
211     *
212     * @param Bounce_Handler_Test $test The object orchestrating the test.
213     *
214     * @return ?array{data:array,html:string}
215     */
216    public function setup_test( Bounce_Handler_Test $test ): ?array {
217
218        if ( ! $this->is_enabled() ) {
219            return null;
220        }
221
222        $params = array();
223
224        $params['email'] = $test->get_email();
225
226        /**
227         * The Newsletter subscriber object.
228         *
229         * @var \TNP_User $tnp_user
230         */
231        $tnp_user = TNP::add_subscriber( $params );
232
233        $tnp_user_status = $tnp_user->status;
234
235        $tnp_user_url = admin_url( 'admin.php?page=newsletter_users_edit&id=' . $tnp_user->id );
236
237        $data                    = array();
238        $data['tnp_user_id']     = $tnp_user->id;
239        $data['tnp_user_status'] = $tnp_user_status;
240
241        $html = '<p>Newsletter <a href="' . $tnp_user_url . '">subscriber ' . $tnp_user->id . '</a> created with status ' . $tnp_user_status . '</p>';
242
243        return array(
244            'data' => $data,
245            'html' => $html,
246        );
247
248    }
249
250    /**
251     * Verify the subscriber has been marked as Bounced.
252     *
253     * @param array{tnp_user_id:int, tnp_user_status:string} $test_data The data generated earlier for the test.
254     *
255     * @return array{success:bool, html:string} containing success boolean and html.
256     */
257    public function verify_test( array $test_data ): ?array {
258
259        if ( ! $this->is_enabled() ) {
260            // This is an odd point to reach.
261            return null;
262        }
263
264        $newsletter = \Newsletter::instance();
265
266        $tnp_user = $newsletter->get_user( $test_data['tnp_user_id'] );
267
268        if ( empty( $tnp_user ) ) {
269            return array(
270                'success' => false,
271                'html'    => "<p>Failed to get test user {$test_data['tnp_user_id']}.</p>",
272            );
273        }
274
275        $tnp_user_status = $tnp_user->status;
276
277        $tnp_user_url = admin_url( 'admin.php?page=newsletter_users_edit&id=' . $tnp_user->id );
278
279        $success = 'B' === $tnp_user_status;
280
281        if ( $success ) {
282            $html = '<p>Newsletter <a href="' . $tnp_user_url . '">subscriber ' . $tnp_user->id . '</a> found with new status ' . $tnp_user_status . '</p>';
283        } else {
284            $html = '<p>Newsletter user status not changed</p>';
285        }
286
287        return array(
288            'success' => $success,
289            'html'    => $html,
290        );
291    }
292
293    /**
294     * Delete the subscriber created for the test, by user id.
295     *
296     * @param array{tnp_user_id: int} $test_data The data created and saved during setup_test().
297     */
298    public function delete_test_data( array $test_data ): bool {
299
300        $user = null;
301
302        if ( isset( $test_data['tnp_user_id'] ) ) {
303
304            $user = \Newsletter::instance()->get_user( $test_data['tnp_user_id'] );
305
306            \Newsletter::instance()->delete_user( $test_data['tnp_user_id'] );
307
308        }
309
310        return ! is_null( $user );
311    }
312
313}