Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.60% covered (warning)
71.60%
58 / 81
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
BounceHandlerActions
71.60% covered (warning)
71.60%
58 / 81
50.00% covered (danger)
50.00%
3 / 6
16.87
0.00% covered (danger)
0.00%
0 / 1
 __construct
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 handleFailingRecipient
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 createEchoNotification
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 notifyGlobalUser
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 unSubscribeUser
64.29% covered (warning)
64.29%
18 / 28
0.00% covered (danger)
0.00%
0 / 1
4.73
 formatHeaders
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\BounceHandler;
4
5use EchoEvent;
6use Exception;
7use ExtensionRegistry;
8use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Title\Title;
11use MediaWiki\WikiMap\WikiMap;
12use User;
13
14/**
15 * Class BounceHandlerActions
16 *
17 * Actions to be done on finding out a failing recipient
18 *
19 * @file
20 * @ingroup Extensions
21 * @author Tony Thomas, Kunal Mehta, Jeff Green
22 * @license GPL-2.0-or-later
23 */
24class BounceHandlerActions {
25
26    /**
27     * @var string
28     */
29    protected $wikiId;
30
31    /**
32     * @var int
33     */
34    protected $bounceRecordPeriod;
35
36    /**
37     * @var int
38     */
39    protected $bounceRecordLimit;
40
41    /**
42     * @var bool
43     */
44    protected $bounceHandlerUnconfirmUsers;
45
46    /**
47     * @var string
48     */
49    protected $emailRaw;
50
51    /**
52     * @param string $wikiId The database id of the failing recipient
53     * @param int $bounceRecordPeriod Time period for which bounce activities are considered
54     *  before un-subscribing
55     * @param int $bounceRecordLimit The number of bounce allowed in the bounceRecordPeriod.
56     * @param bool $bounceHandlerUnconfirmUsers Enable/Disable user un-subscribe action
57     * @param string $emailRaw The raw bounce Email
58     * @throws Exception
59     */
60    public function __construct(
61        $wikiId, $bounceRecordPeriod, $bounceRecordLimit, $bounceHandlerUnconfirmUsers, $emailRaw
62    ) {
63        if ( $wikiId !== WikiMap::getCurrentWikiId() ) {
64            // We want to use the User class methods, which make no sense on the wrong wiki
65            throw new Exception( "BounceHandlerActions constructed for a foreign wiki." );
66        }
67
68        $this->wikiId = $wikiId;
69        $this->bounceRecordPeriod = $bounceRecordPeriod;
70        $this->bounceRecordLimit = $bounceRecordLimit;
71        $this->bounceHandlerUnconfirmUsers = $bounceHandlerUnconfirmUsers;
72        $this->emailRaw = $emailRaw;
73    }
74
75    /**
76     * Perform actions on users who failed to receive emails in a given period
77     *
78     * @param array $failedUser The details of the failing user
79     * @param array $emailHeaders Email headers
80     * @return bool
81     */
82    public function handleFailingRecipient( array $failedUser, $emailHeaders ) {
83        if ( $this->bounceHandlerUnconfirmUsers ) {
84            $originalEmail = $failedUser['rawEmail'];
85            $bounceValidPeriod = time() - $this->bounceRecordPeriod; // Unix
86
87            $dbr = ProcessBounceEmails::getBounceRecordDB( DB_REPLICA, $this->wikiId );
88
89            $totalBounces = $dbr->selectRowCount( 'bounce_records',
90                '*',
91                [
92                    'br_user_email' => $originalEmail,
93                    'br_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $bounceValidPeriod ) )
94                ],
95                __METHOD__,
96                [ 'LIMIT' => $this->bounceRecordLimit ]
97            );
98
99            if ( $totalBounces >= $this->bounceRecordLimit ) {
100                $this->unSubscribeUser( $failedUser, $emailHeaders );
101            }
102        }
103
104        return true;
105    }
106
107    /**
108     * Function to trigger Echo notifications
109     *
110     * @param int $userId ID of user to be notified
111     * @param string $email un-subscribed email address used in notification
112     */
113    public function createEchoNotification( $userId, $email ) {
114        if ( ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) {
115            EchoEvent::create( [
116                'type' => 'unsubscribe-bouncehandler',
117                'extra' => [
118                    'failed-user-id' => $userId,
119                    'failed-email' => $email,
120                ],
121            ] );
122        }
123    }
124
125    /**
126     * Function to inject Echo notification to the last source of bounce for an
127     * unsubscribed Global user
128     *
129     * @param int $bounceUserId
130     * @param string $originalEmail
131     */
132    public function notifyGlobalUser( $bounceUserId, $originalEmail ) {
133        $params = [
134            'failed-user-id' => $bounceUserId,
135            'failed-email' => $originalEmail,
136            'wikiId' => $this->wikiId,
137            'bounceRecordPeriod' => $this->bounceRecordPeriod,
138            'bounceRecordLimit' => $this->bounceRecordLimit,
139            'bounceHandlerUnconfirmUsers' => $this->bounceHandlerUnconfirmUsers,
140            'emailRaw' => $this->emailRaw,
141        ];
142        $title = Title::newFromText( 'BounceHandler Global user notification Job' );
143        $job = new BounceHandlerNotificationJob( $title, $params );
144        MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup( $this->wikiId )->push( $job );
145    }
146
147    /**
148     * Function to Un-subscribe a failing recipient
149     *
150     * @param array $failedUser The details of the failing user
151     * @param array $emailHeaders Email headers
152     */
153    public function unSubscribeUser( array $failedUser, $emailHeaders ) {
154        // Un-subscribe the user
155        $originalEmail = $failedUser['rawEmail'];
156        $bounceUserId = $failedUser['rawUserId'];
157
158        $user = User::newFromId( $bounceUserId );
159        $stats = \MediaWiki\MediaWikiServices::getInstance()->getStatsdDataFactory();
160        // Handle the central account email status (if applicable)
161        $unsubscribeLocalUser = true;
162        if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
163            $caUser = CentralAuthUser::getPrimaryInstance( $user );
164            if ( $caUser->isAttached() ) {
165                $unsubscribeLocalUser = false;
166                $caUser->setEmailAuthenticationTimestamp( null );
167                $caUser->saveSettings();
168                $this->notifyGlobalUser( $bounceUserId, $originalEmail );
169                wfDebugLog( 'BounceHandler',
170                    "Un-subscribed global user {$caUser->getName()} <$originalEmail> for " .
171                        "exceeding Bounce Limit $this->bounceRecordLimit.\nProcessed Headers:\n" .
172                        $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw"
173                );
174                $stats->increment( 'bouncehandler.unsub.global' );
175            }
176        }
177        if ( $unsubscribeLocalUser ) {
178            // Invalidate the email-id of a local user
179            $user->setEmailAuthenticationTimestamp( null );
180            $user->saveSettings();
181            $this->createEchoNotification( $bounceUserId, $originalEmail );
182            wfDebugLog( 'BounceHandler',
183                "Un-subscribed {$user->getName()} <$originalEmail> for exceeding Bounce limit " .
184                    "$this->bounceRecordLimit.\nProcessed Headers:\n" .
185                    $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw"
186            );
187            $stats->increment( 'bouncehandler.unsub.local' );
188        }
189    }
190
191    /**
192     * Turns a keyed array into "Key: Value" newline split string
193     *
194     * @param array $emailHeaders
195     * @return string
196     */
197    private function formatHeaders( $emailHeaders ) {
198        return implode(
199            "\n",
200            array_map(
201                static function ( $v, $k ) {
202                    return "$k$v";
203                },
204                $emailHeaders,
205                array_keys( $emailHeaders )
206            )
207        );
208    }
209
210}