Completed
Branch master (8ef871)
by
unknown
29:40
created

EmailNotification   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 73
lcom 1
cbo 16
dl 0
loc 465
rs 2.459
c 1
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A updateWatchlistTimestamp() 0 16 3
C notifyOnPageChange() 0 55 13
C actuallyNotifyOnPageChange() 0 72 21
C canSendUserTalkEmail() 0 30 13
D composeCommonMailtext() 0 108 12
A compose() 0 13 3
A sendMails() 0 6 2
B sendPersonalised() 0 31 4
A sendImpersonal() 0 20 2

How to fix   Complexity   

Complex Class

Complex classes like EmailNotification often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EmailNotification, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Classes used to send e-mails
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author <[email protected]>
22
 * @author <[email protected]>
23
 * @author Tim Starling
24
 * @author Luke Welling [email protected]
25
 */
26
27
/**
28
 * This module processes the email notifications when the current page is
29
 * changed. It looks up the table watchlist to find out which users are watching
30
 * that page.
31
 *
32
 * The current implementation sends independent emails to each watching user for
33
 * the following reason:
34
 *
35
 * - Each watching user will be notified about the page edit time expressed in
36
 * his/her local time (UTC is shown additionally). To achieve this, we need to
37
 * find the individual timeoffset of each watching user from the preferences..
38
 *
39
 * Suggested improvement to slack down the number of sent emails: We could think
40
 * of sending out bulk mails (bcc:user1,user2...) for all these users having the
41
 * same timeoffset in their preferences.
42
 *
43
 * Visit the documentation pages under http://meta.wikipedia.com/Enotif
44
 */
45
class EmailNotification {
46
47
	/**
48
	 * Notification is due to user's user talk being edited
49
	 */
50
	const USER_TALK = 'user_talk';
51
	/**
52
	 * Notification is due to a watchlisted page being edited
53
	 */
54
	const WATCHLIST = 'watchlist';
55
	/**
56
	 * Notification because user is notified for all changes
57
	 */
58
	const ALL_CHANGES = 'all_changes';
59
60
	protected $subject, $body, $replyto, $from;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
61
	protected $timestamp, $summary, $minorEdit, $oldid, $composed_common, $pageStatus;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
62
	protected $mailTargets = [];
63
64
	/**
65
	 * @var Title
66
	 */
67
	protected $title;
68
69
	/**
70
	 * @var User
71
	 */
72
	protected $editor;
73
74
	/**
75
	 * @deprecated since 1.27 use WatchedItemStore::updateNotificationTimestamp directly
76
	 *
77
	 * @param User $editor The editor that triggered the update.  Their notification
78
	 *  timestamp will not be updated(they have already seen it)
79
	 * @param LinkTarget $linkTarget The link target of the title to update timestamps for
80
	 * @param string $timestamp Set the update timestamp to this value
81
	 *
82
	 * @return int[] Array of user IDs
83
	 */
84
	public static function updateWatchlistTimestamp(
85
		User $editor,
86
		LinkTarget $linkTarget,
87
		$timestamp
88
	) {
89
		// wfDeprecated( __METHOD__, '1.27' );
90
		$config = RequestContext::getMain()->getConfig();
91
		if ( !$config->get( 'EnotifWatchlist' ) && !$config->get( 'ShowUpdatedMarker' ) ) {
92
			return [];
93
		}
94
		return WatchedItemStore::getDefaultInstance()->updateNotificationTimestamp(
95
			$editor,
96
			$linkTarget,
97
			$timestamp
98
		);
99
	}
100
101
	/**
102
	 * Send emails corresponding to the user $editor editing the page $title.
103
	 *
104
	 * May be deferred via the job queue.
105
	 *
106
	 * @param User $editor
107
	 * @param Title $title
108
	 * @param string $timestamp
109
	 * @param string $summary
110
	 * @param bool $minorEdit
111
	 * @param bool $oldid (default: false)
112
	 * @param string $pageStatus (default: 'changed')
113
	 */
114
	public function notifyOnPageChange( $editor, $title, $timestamp, $summary,
115
		$minorEdit, $oldid = false, $pageStatus = 'changed'
116
	) {
117
		global $wgEnotifMinorEdits, $wgUsersNotifiedOnAllChanges, $wgEnotifUserTalk;
118
119
		if ( $title->getNamespace() < 0 ) {
120
			return;
121
		}
122
123
		// update wl_notificationtimestamp for watchers
124
		$config = RequestContext::getMain()->getConfig();
125
		$watchers = [];
126
		if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
127
			$watchers = WatchedItemStore::getDefaultInstance()->updateNotificationTimestamp(
128
				$editor,
129
				$title,
130
				$timestamp
131
			);
132
		}
133
134
		$sendEmail = true;
135
		// $watchers deals with $wgEnotifWatchlist.
136
		// If nobody is watching the page, and there are no users notified on all changes
137
		// don't bother creating a job/trying to send emails, unless it's a
138
		// talk page with an applicable notification.
139
		if ( !count( $watchers ) && !count( $wgUsersNotifiedOnAllChanges ) ) {
140
			$sendEmail = false;
141
			// Only send notification for non minor edits, unless $wgEnotifMinorEdits
142
			if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) {
143
				$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
144
				if ( $wgEnotifUserTalk
145
					&& $isUserTalkPage
146
					&& $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
147
				) {
148
					$sendEmail = true;
149
				}
150
			}
151
		}
152
153
		if ( $sendEmail ) {
154
			JobQueueGroup::singleton()->lazyPush( new EnotifNotifyJob(
155
				$title,
156
				[
157
					'editor' => $editor->getName(),
158
					'editorID' => $editor->getID(),
159
					'timestamp' => $timestamp,
160
					'summary' => $summary,
161
					'minorEdit' => $minorEdit,
162
					'oldid' => $oldid,
163
					'watchers' => $watchers,
164
					'pageStatus' => $pageStatus
165
				]
166
			) );
167
		}
168
	}
169
170
	/**
171
	 * Immediate version of notifyOnPageChange().
172
	 *
173
	 * Send emails corresponding to the user $editor editing the page $title.
174
	 *
175
	 * @note Do not call directly. Use notifyOnPageChange so that wl_notificationtimestamp is updated.
176
	 * @param User $editor
177
	 * @param Title $title
178
	 * @param string $timestamp Edit timestamp
179
	 * @param string $summary Edit summary
180
	 * @param bool $minorEdit
181
	 * @param int $oldid Revision ID
182
	 * @param array $watchers Array of user IDs
183
	 * @param string $pageStatus
184
	 * @throws MWException
185
	 */
186
	public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit,
187
		$oldid, $watchers, $pageStatus = 'changed' ) {
188
		# we use $wgPasswordSender as sender's address
189
		global $wgUsersNotifiedOnAllChanges;
190
		global $wgEnotifWatchlist, $wgBlockDisablesLogin;
191
		global $wgEnotifMinorEdits, $wgEnotifUserTalk;
192
193
		# The following code is only run, if several conditions are met:
194
		# 1. EmailNotification for pages (other than user_talk pages) must be enabled
195
		# 2. minor edits (changes) are only regarded if the global flag indicates so
196
197
		$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
198
199
		$this->title = $title;
200
		$this->timestamp = $timestamp;
201
		$this->summary = $summary;
202
		$this->minorEdit = $minorEdit;
203
		$this->oldid = $oldid;
204
		$this->editor = $editor;
205
		$this->composed_common = false;
206
		$this->pageStatus = $pageStatus;
207
208
		$formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
209
210
		Hooks::run( 'UpdateUserMailerFormattedPageStatus', [ &$formattedPageStatus ] );
211
		if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
212
			throw new MWException( 'Not a valid page status!' );
213
		}
214
215
		$userTalkId = false;
216
217
		if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) {
218
			if ( $wgEnotifUserTalk
219
				&& $isUserTalkPage
220
				&& $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
221
			) {
222
				$targetUser = User::newFromName( $title->getText() );
223
				$this->compose( $targetUser, self::USER_TALK );
0 ignored issues
show
Security Bug introduced by
It seems like $targetUser defined by \User::newFromName($title->getText()) on line 222 can also be of type false; however, EmailNotification::compose() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
224
				$userTalkId = $targetUser->getId();
225
			}
226
227
			if ( $wgEnotifWatchlist ) {
228
				// Send updates to watchers other than the current editor
229
				// and don't send to watchers who are blocked and cannot login
230
				$userArray = UserArray::newFromIDs( $watchers );
231
				foreach ( $userArray as $watchingUser ) {
0 ignored issues
show
Bug introduced by
The expression $userArray of type object<UserArrayFromResult>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
232
					if ( $watchingUser->getOption( 'enotifwatchlistpages' )
233
						&& ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
234
						&& $watchingUser->isEmailConfirmed()
235
						&& $watchingUser->getID() != $userTalkId
236
						&& !in_array( $watchingUser->getName(), $wgUsersNotifiedOnAllChanges )
237
						&& !( $wgBlockDisablesLogin && $watchingUser->isBlocked() )
238
					) {
239
						if ( Hooks::run( 'SendWatchlistEmailNotification', [ $watchingUser, $title, $this ] ) ) {
240
							$this->compose( $watchingUser, self::WATCHLIST );
241
						}
242
					}
243
				}
244
			}
245
		}
246
247
		foreach ( $wgUsersNotifiedOnAllChanges as $name ) {
248
			if ( $editor->getName() == $name ) {
249
				// No point notifying the user that actually made the change!
250
				continue;
251
			}
252
			$user = User::newFromName( $name );
253
			$this->compose( $user, self::ALL_CHANGES );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($name) on line 252 can also be of type false; however, EmailNotification::compose() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
254
		}
255
256
		$this->sendMails();
257
	}
258
259
	/**
260
	 * @param User $editor
261
	 * @param Title $title
262
	 * @param bool $minorEdit
263
	 * @return bool
264
	 */
265
	private function canSendUserTalkEmail( $editor, $title, $minorEdit ) {
266
		global $wgEnotifUserTalk, $wgBlockDisablesLogin;
267
		$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
268
269
		if ( $wgEnotifUserTalk && $isUserTalkPage ) {
270
			$targetUser = User::newFromName( $title->getText() );
271
272
			if ( !$targetUser || $targetUser->isAnon() ) {
273
				wfDebug( __METHOD__ . ": user talk page edited, but user does not exist\n" );
274
			} elseif ( $targetUser->getId() == $editor->getId() ) {
275
				wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent\n" );
276
			} elseif ( $wgBlockDisablesLogin && $targetUser->isBlocked() ) {
277
				wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent\n" );
278
			} elseif ( $targetUser->getOption( 'enotifusertalkpages' )
279
				&& ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
280
			) {
281
				if ( !$targetUser->isEmailConfirmed() ) {
282
					wfDebug( __METHOD__ . ": talk page owner doesn't have validated email\n" );
283
				} elseif ( !Hooks::run( 'AbortTalkPageEmailNotification', [ $targetUser, $title ] ) ) {
284
					wfDebug( __METHOD__ . ": talk page update notification is aborted for this user\n" );
285
				} else {
286
					wfDebug( __METHOD__ . ": sending talk page update notification\n" );
287
					return true;
288
				}
289
			} else {
290
				wfDebug( __METHOD__ . ": talk page owner doesn't want notifications\n" );
291
			}
292
		}
293
		return false;
294
	}
295
296
	/**
297
	 * Generate the generic "this page has been changed" e-mail text.
298
	 */
299
	private function composeCommonMailtext() {
300
		global $wgPasswordSender, $wgNoReplyAddress;
301
		global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
302
		global $wgEnotifImpersonal, $wgEnotifUseRealName;
303
304
		$this->composed_common = true;
305
306
		# You as the WikiAdmin and Sysops can make use of plenty of
307
		# named variables when composing your notification emails while
308
		# simply editing the Meta pages
309
310
		$keys = [];
311
		$postTransformKeys = [];
312
		$pageTitleUrl = $this->title->getCanonicalURL();
313
		$pageTitle = $this->title->getPrefixedText();
314
315
		if ( $this->oldid ) {
316
			// Always show a link to the diff which triggered the mail. See bug 32210.
317
			$keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff',
318
					$this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) )
319
					->inContentLanguage()->text();
320
321
			if ( !$wgEnotifImpersonal ) {
322
				// For personal mail, also show a link to the diff of all changes
323
				// since last visited.
324
				$keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited',
325
						$this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] ) )
326
						->inContentLanguage()->text();
327
			}
328
			$keys['$OLDID'] = $this->oldid;
329
			// Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
330
			$keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
331
		} else {
332
			# clear $OLDID placeholder in the message template
333
			$keys['$OLDID'] = '';
334
			$keys['$NEWPAGE'] = '';
335
			// Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
336
			$keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
337
		}
338
339
		$keys['$PAGETITLE'] = $this->title->getPrefixedText();
340
		$keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
341
		$keys['$PAGEMINOREDIT'] = $this->minorEdit ?
342
			wfMessage( 'minoredit' )->inContentLanguage()->text() : '';
343
		$keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
344
345
		if ( $this->editor->isAnon() ) {
346
			# real anon (user:xxx.xxx.xxx.xxx)
347
			$keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
348
				->inContentLanguage()->text();
349
			$keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
350
351
		} else {
352
			$keys['$PAGEEDITOR'] = $wgEnotifUseRealName && $this->editor->getRealName() !== ''
353
				? $this->editor->getRealName() : $this->editor->getName();
354
			$emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
355
			$keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
356
		}
357
358
		$keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
359
		$keys['$HELPPAGE'] = wfExpandUrl(
360
			Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
361
		);
362
363
		# Replace this after transforming the message, bug 35019
364
		$postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
365
366
		// Now build message's subject and body
367
368
		// Messages:
369
		// enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
370
		// enotif_subject_restored, enotif_subject_changed
371
		$this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
372
			->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
373
374
		// Messages:
375
		// enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
376
		// enotif_body_intro_restored, enotif_body_intro_changed
377
		$keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
378
			->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
379
			->text();
380
381
		$body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
382
		$body = strtr( $body, $keys );
383
		$body = MessageCache::singleton()->transform( $body, false, null, $this->title );
384
		$this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
385
386
		# Reveal the page editor's address as REPLY-TO address only if
387
		# the user has not opted-out and the option is enabled at the
388
		# global configuration level.
389
		$adminAddress = new MailAddress( $wgPasswordSender,
390
			wfMessage( 'emailsender' )->inContentLanguage()->text() );
391
		if ( $wgEnotifRevealEditorAddress
392
			&& ( $this->editor->getEmail() != '' )
393
			&& $this->editor->getOption( 'enotifrevealaddr' )
394
		) {
395
			$editorAddress = MailAddress::newFromUser( $this->editor );
396
			if ( $wgEnotifFromEditor ) {
397
				$this->from = $editorAddress;
398
			} else {
399
				$this->from = $adminAddress;
400
				$this->replyto = $editorAddress;
401
			}
402
		} else {
403
			$this->from = $adminAddress;
404
			$this->replyto = new MailAddress( $wgNoReplyAddress );
405
		}
406
	}
407
408
	/**
409
	 * Compose a mail to a given user and either queue it for sending, or send it now,
410
	 * depending on settings.
411
	 *
412
	 * Call sendMails() to send any mails that were queued.
413
	 * @param User $user
414
	 * @param string $source
415
	 */
416
	function compose( $user, $source ) {
417
		global $wgEnotifImpersonal;
418
419
		if ( !$this->composed_common ) {
420
			$this->composeCommonMailtext();
421
		}
422
423
		if ( $wgEnotifImpersonal ) {
424
			$this->mailTargets[] = MailAddress::newFromUser( $user );
425
		} else {
426
			$this->sendPersonalised( $user, $source );
427
		}
428
	}
429
430
	/**
431
	 * Send any queued mails
432
	 */
433
	function sendMails() {
434
		global $wgEnotifImpersonal;
435
		if ( $wgEnotifImpersonal ) {
436
			$this->sendImpersonal( $this->mailTargets );
437
		}
438
	}
439
440
	/**
441
	 * Does the per-user customizations to a notification e-mail (name,
442
	 * timestamp in proper timezone, etc) and sends it out.
443
	 * Returns true if the mail was sent successfully.
444
	 *
445
	 * @param User $watchingUser
446
	 * @param string $source
447
	 * @return bool
448
	 * @private
449
	 */
450
	function sendPersonalised( $watchingUser, $source ) {
451
		global $wgContLang, $wgEnotifUseRealName;
452
		// From the PHP manual:
453
		//   Note: The to parameter cannot be an address in the form of
454
		//   "Something <[email protected]>". The mail command will not parse
455
		//   this properly while talking with the MTA.
456
		$to = MailAddress::newFromUser( $watchingUser );
457
458
		# $PAGEEDITDATE is the time and date of the page change
459
		# expressed in terms of individual local time of the notification
460
		# recipient, i.e. watching user
461
		$body = str_replace(
462
			[ '$WATCHINGUSERNAME',
463
				'$PAGEEDITDATE',
464
				'$PAGEEDITTIME' ],
465
			[ $wgEnotifUseRealName && $watchingUser->getRealName() !== ''
466
				? $watchingUser->getRealName() : $watchingUser->getName(),
467
				$wgContLang->userDate( $this->timestamp, $watchingUser ),
468
				$wgContLang->userTime( $this->timestamp, $watchingUser ) ],
469
			$this->body );
470
471
		$headers = [];
472
		if ( $source === self::WATCHLIST ) {
473
			$headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
474
		}
475
476
		return UserMailer::send( $to, $this->from, $this->subject, $body, [
477
			'replyTo' => $this->replyto,
478
			'headers' => $headers,
479
		] );
480
	}
481
482
	/**
483
	 * Same as sendPersonalised but does impersonal mail suitable for bulk
484
	 * mailing.  Takes an array of MailAddress objects.
485
	 * @param MailAddress[] $addresses
486
	 * @return Status|null
487
	 */
488
	function sendImpersonal( $addresses ) {
489
		global $wgContLang;
490
491
		if ( empty( $addresses ) ) {
492
			return null;
493
		}
494
495
		$body = str_replace(
496
			[ '$WATCHINGUSERNAME',
497
				'$PAGEEDITDATE',
498
				'$PAGEEDITTIME' ],
499
			[ wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
500
				$wgContLang->date( $this->timestamp, false, false ),
501
				$wgContLang->time( $this->timestamp, false, false ) ],
502
			$this->body );
503
504
		return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
505
			'replyTo' => $this->replyto,
506
		] );
507
	}
508
509
}
510