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 ExtensionRegistry;
7use InvalidArgumentException;
8use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12use MediaWiki\WikiMap\WikiMap;
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     */
59    public function __construct(
60        $wikiId, $bounceRecordPeriod, $bounceRecordLimit, $bounceHandlerUnconfirmUsers, $emailRaw
61    ) {
62        if ( $wikiId !== WikiMap::getCurrentWikiId() ) {
63            // We want to use the User class methods, which make no sense on the wrong wiki
64            throw new InvalidArgumentException( "BounceHandlerActions constructed for a foreign wiki." );
65        }
66
67        $this->wikiId = $wikiId;
68        $this->bounceRecordPeriod = $bounceRecordPeriod;
69        $this->bounceRecordLimit = $bounceRecordLimit;
70        $this->bounceHandlerUnconfirmUsers = $bounceHandlerUnconfirmUsers;
71        $this->emailRaw = $emailRaw;
72    }
73
74    /**
75     * Perform actions on users who failed to receive emails in a given period
76     *
77     * @param array $failedUser The details of the failing user
78     * @param array $emailHeaders Email headers
79     * @return bool
80     */
81    public function handleFailingRecipient( array $failedUser, $emailHeaders ) {
82        if ( $this->bounceHandlerUnconfirmUsers ) {
83            $originalEmail = $failedUser['rawEmail'];
84            $bounceValidPeriod = time() - $this->bounceRecordPeriod; // Unix
85
86            $dbr = ProcessBounceEmails::getBounceRecordDB( DB_REPLICA, $this->wikiId );
87
88            $totalBounces = $dbr->newSelectQueryBuilder()
89                ->select( '*' )
90                ->from( 'bounce_records' )
91                ->where( [
92                    'br_user_email' => $originalEmail,
93                    $dbr->expr( 'br_timestamp', '>=', $dbr->timestamp( $bounceValidPeriod ) )
94                ] )
95                ->limit( $this->bounceRecordLimit )
96                ->caller( __METHOD__ )->fetchRowCount();
97
98            if ( $totalBounces >= $this->bounceRecordLimit ) {
99                $this->unSubscribeUser( $failedUser, $emailHeaders );
100            }
101        }
102
103        return true;
104    }
105
106    /**
107     * Function to trigger Echo notifications
108     *
109     * @param int $userId ID of user to be notified
110     * @param string $email un-subscribed email address used in notification
111     */
112    public function createEchoNotification( $userId, $email ) {
113        if ( ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) {
114            EchoEvent::create( [
115                'type' => 'unsubscribe-bouncehandler',
116                'extra' => [
117                    'failed-user-id' => $userId,
118                    'failed-email' => $email,
119                ],
120            ] );
121        }
122    }
123
124    /**
125     * Function to inject Echo notification to the last source of bounce for an
126     * unsubscribed Global user
127     *
128     * @param int $bounceUserId
129     * @param string $originalEmail
130     */
131    public function notifyGlobalUser( $bounceUserId, $originalEmail ) {
132        $params = [
133            'failed-user-id' => $bounceUserId,
134            'failed-email' => $originalEmail,
135            'wikiId' => $this->wikiId,
136            'bounceRecordPeriod' => $this->bounceRecordPeriod,
137            'bounceRecordLimit' => $this->bounceRecordLimit,
138            'bounceHandlerUnconfirmUsers' => $this->bounceHandlerUnconfirmUsers,
139            'emailRaw' => $this->emailRaw,
140        ];
141        $title = Title::newFromText( 'BounceHandler Global user notification Job' );
142        $job = new BounceHandlerNotificationJob( $title, $params );
143        MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup( $this->wikiId )->push( $job );
144    }
145
146    /**
147     * Function to Un-subscribe a failing recipient
148     *
149     * @param array $failedUser The details of the failing user
150     * @param array $emailHeaders Email headers
151     */
152    public function unSubscribeUser( array $failedUser, $emailHeaders ) {
153        // Un-subscribe the user
154        $originalEmail = $failedUser['rawEmail'];
155        $bounceUserId = $failedUser['rawUserId'];
156
157        $user = User::newFromId( $bounceUserId );
158        $stats = \MediaWiki\MediaWikiServices::getInstance()->getStatsdDataFactory();
159        // Handle the central account email status (if applicable)
160        $unsubscribeLocalUser = true;
161        if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
162            $caUser = CentralAuthUser::getPrimaryInstance( $user );
163            if ( $caUser->isAttached() ) {
164                $unsubscribeLocalUser = false;
165                $caUser->setEmailAuthenticationTimestamp( null );
166                $caUser->saveSettings();
167                $this->notifyGlobalUser( $bounceUserId, $originalEmail );
168                wfDebugLog( 'BounceHandler',
169                    "Un-subscribed global user {$caUser->getName()} <$originalEmail> for " .
170                        "exceeding Bounce Limit $this->bounceRecordLimit.\nProcessed Headers:\n" .
171                        $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw"
172                );
173                $stats->increment( 'bouncehandler.unsub.global' );
174            }
175        }
176        if ( $unsubscribeLocalUser ) {
177            // Invalidate the email-id of a local user
178            $user->setEmailAuthenticationTimestamp( null );
179            $user->saveSettings();
180            $this->createEchoNotification( $bounceUserId, $originalEmail );
181            wfDebugLog( 'BounceHandler',
182                "Un-subscribed {$user->getName()} <$originalEmail> for exceeding Bounce limit " .
183                    "$this->bounceRecordLimit.\nProcessed Headers:\n" .
184                    $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw"
185            );
186            $stats->increment( 'bouncehandler.unsub.local' );
187        }
188    }
189
190    /**
191     * Turns a keyed array into "Key: Value" newline split string
192     *
193     * @param array $emailHeaders
194     * @return string
195     */
196    private function formatHeaders( $emailHeaders ) {
197        return implode(
198            "\n",
199            array_map(
200                static function ( $v, $k ) {
201                    return "$k$v";
202                },
203                $emailHeaders,
204                array_keys( $emailHeaders )
205            )
206        );
207    }
208
209}