MediaWiki REL1_41
EmailUser.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Mail;
22
23use MailAddress;
36use StatusValue;
37use UnexpectedValueException;
41
47class EmailUser {
51 public const CONSTRUCTOR_OPTIONS = [
57 ];
58
60 private ServiceOptions $options;
62 private HookRunner $hookRunner;
64 private UserOptionsLookup $userOptionsLookup;
66 private CentralIdLookup $centralIdLookup;
68 private UserFactory $userFactory;
70 private IEmailer $emailer;
72 private IMessageFormatterFactory $messageFormatterFactory;
74 private ITextFormatter $contLangMsgFormatter;
75
77 private Authority $sender;
78
80 private string $editToken = '';
81
93 public function __construct(
94 ServiceOptions $options,
95 HookContainer $hookContainer,
96 UserOptionsLookup $userOptionsLookup,
97 CentralIdLookup $centralIdLookup,
98 UserFactory $userFactory,
99 IEmailer $emailer,
100 IMessageFormatterFactory $messageFormatterFactory,
101 ITextFormatter $contLangMsgFormatter,
102 Authority $sender
103 ) {
104 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
105 $this->options = $options;
106 $this->hookRunner = new HookRunner( $hookContainer );
107 $this->userOptionsLookup = $userOptionsLookup;
108 $this->centralIdLookup = $centralIdLookup;
109 $this->userFactory = $userFactory;
110 $this->emailer = $emailer;
111 $this->messageFormatterFactory = $messageFormatterFactory;
112 $this->contLangMsgFormatter = $contLangMsgFormatter;
113
114 $this->sender = $sender;
115 }
116
124 public function validateTarget( UserEmailContact $target ): StatusValue {
125 $targetIdentity = $target->getUser();
126
127 if ( !$targetIdentity->getId() ) {
128 return StatusValue::newFatal( 'emailnotarget' );
129 }
130
131 if ( !$target->isEmailConfirmed() ) {
132 return StatusValue::newFatal( 'noemailtext' );
133 }
134
135 $targetUser = $this->userFactory->newFromUserIdentity( $targetIdentity );
136 if ( !$targetUser->canReceiveEmail() ) {
137 return StatusValue::newFatal( 'nowikiemailtext' );
138 }
139
140 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
141 if (
142 !$this->userOptionsLookup->getOption( $targetIdentity, 'email-allow-new-users' ) &&
143 $senderUser->isNewbie()
144 ) {
145 return StatusValue::newFatal( 'nowikiemailtext' );
146 }
147
148 $muteList = $this->userOptionsLookup->getOption(
149 $targetIdentity,
150 'email-blacklist',
151 ''
152 );
153 if ( $muteList ) {
154 $muteList = MultiUsernameFilter::splitIds( $muteList );
155 $senderId = $this->centralIdLookup->centralIdFromLocalUser( $this->sender->getUser() );
156 if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
157 return StatusValue::newFatal( 'nowikiemailtext' );
158 }
159 }
160
161 return StatusValue::newGood();
162 }
163
170 public function canSend(): StatusValue {
171 if (
172 !$this->options->get( MainConfigNames::EnableEmail ) ||
173 !$this->options->get( MainConfigNames::EnableUserEmail )
174 ) {
175 return StatusValue::newFatal( 'usermaildisabled' );
176 }
177
178 $user = $this->userFactory->newFromAuthority( $this->sender );
179
180 // Run this before checking 'sendemail' permission
181 // to show appropriate message to anons (T160309)
182 if ( !$user->isEmailConfirmed() ) {
183 return StatusValue::newFatal( 'mailnologin' );
184 }
185
186 $status = PermissionStatus::newGood();
187 if ( !$this->sender->isDefinitelyAllowed( 'sendemail', $status ) ) {
188 return $status;
189 }
190
191 $hookErr = false;
192
193 // TODO Remove deprecated hooks
194 $this->hookRunner->onUserCanSendEmail( $user, $hookErr );
195 $this->hookRunner->onEmailUserPermissionsErrors( $user, $this->editToken, $hookErr );
196 if ( is_array( $hookErr ) ) {
197 // SpamBlacklist uses null for the third element, and there might be more handlers not using an array.
198 $msgParamsArray = is_array( $hookErr[2] ) ? $hookErr[2] : [];
199 $ret = StatusValue::newFatal( $hookErr[1], ...$msgParamsArray );
200 $ret->value = $hookErr[0];
201 return $ret;
202 }
203
204 return StatusValue::newGood();
205 }
206
213 public function authorizeSend(): StatusValue {
214 $status = $this->canSend();
215 if ( !$status->isOK() ) {
216 return $status;
217 }
218
219 $status = PermissionStatus::newGood();
220 if ( !$this->sender->authorizeAction( 'sendemail', $status ) ) {
221 return $status;
222 }
223
224 $hookRes = $this->hookRunner->onEmailUserAuthorizeSend( $this->sender, $status );
225 if ( !$hookRes && !$status->isGood() ) {
226 return $status;
227 }
228
229 return StatusValue::newGood();
230 }
231
242 public function sendEmailUnsafe(
243 UserEmailContact $target,
244 string $subject,
245 string $text,
246 bool $CCMe,
247 string $langCode
248 ): StatusValue {
249 $senderIdentity = $this->sender->getUser();
250 $targetStatus = $this->validateTarget( $target );
251 if ( !$targetStatus->isGood() ) {
252 return $targetStatus;
253 }
254
255 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
256
257 $toAddress = MailAddress::newFromUser( $target );
258 $fromAddress = MailAddress::newFromUser( $senderUser );
259
260 // Add a standard footer and trim up trailing newlines
261 $text = rtrim( $text ) . "\n\n-- \n";
262 $text .= $this->contLangMsgFormatter->format(
263 MessageValue::new( 'emailuserfooter', [ $fromAddress->name, $toAddress->name ] )
264 );
265
266 if ( $this->options->get( MainConfigNames::EnableSpecialMute ) ) {
267 $text .= "\n" . $this->contLangMsgFormatter->format(
268 MessageValue::new(
269 'specialmute-email-footer',
270 [
271 $this->getSpecialMuteCanonicalURL( $senderIdentity->getName() ),
272 $senderIdentity->getName()
273 ]
274 )
275 );
276 }
277
278 $error = false;
279 // TODO Remove deprecated ugly hook
280 if ( !$this->hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
281 if ( $error instanceof StatusValue ) {
282 return $error;
283 } elseif ( $error === false || $error === '' || $error === [] ) {
284 // Possibly to tell HTMLForm to pretend there was no submission?
285 return StatusValue::newFatal( 'hookaborted' );
286 } elseif ( $error === true ) {
287 // Hook sent the mail itself and indicates success?
288 return StatusValue::newGood();
289 } elseif ( is_array( $error ) ) {
290 $status = StatusValue::newGood();
291 foreach ( $error as $e ) {
292 $status->fatal( $e );
293 }
294 return $status;
295 } elseif ( $error instanceof MessageSpecifier ) {
296 return StatusValue::newFatal( $error );
297 } else {
298 // Setting $error to something else was deprecated in 1.29 and
299 // removed in 1.36, and so an exception is now thrown
300 $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
301 throw new UnexpectedValueException(
302 'EmailUser hook set $error to unsupported type ' . $type
303 );
304 }
305 }
306
307 $hookStatus = StatusValue::newGood();
308 $hookRes = $this->hookRunner->onEmailUserSendEmail(
309 $this->sender,
310 $fromAddress,
311 $target,
312 $toAddress,
313 $subject,
314 $text,
315 $hookStatus
316 );
317 if ( !$hookRes && !$hookStatus->isGood() ) {
318 return $hookStatus;
319 }
320
321 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $fromAddress );
322
323 $status = $this->emailer->send(
324 $toAddress,
325 $mailFrom,
326 $subject,
327 $text,
328 null,
329 [ 'replyTo' => $replyTo ]
330 );
331
332 if ( !$status->isGood() ) {
333 return $status;
334 }
335
336 // if the user requested a copy of this mail, do this now,
337 // unless they are emailing themselves, in which case one
338 // copy of the message is sufficient.
339 if ( $CCMe && !$toAddress->equals( $fromAddress ) ) {
340 $userMsgFormatter = $this->messageFormatterFactory->getTextFormatter( $langCode );
341 $ccTo = $fromAddress;
342 $ccFrom = $fromAddress;
343 $ccSubject = $userMsgFormatter->format(
344 MessageValue::new( 'emailccsubject' )->plaintextParams(
345 $target->getUser()->getName(),
346 $subject
347 )
348 );
349 $ccText = $text;
350
351 $this->hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
352
353 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $ccFrom );
354
355 $ccStatus = $this->emailer->send(
356 $ccTo,
357 $mailFrom,
358 $ccSubject,
359 $ccText,
360 null,
361 [ 'replyTo' => $replyTo ]
362 );
363 $status->merge( $ccStatus );
364 }
365
366 $this->hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
367
368 return $status;
369 }
370
376 private function getFromAndReplyTo( MailAddress $fromAddress ): array {
377 if ( $this->options->get( MainConfigNames::UserEmailUseReplyTo ) ) {
386 $mailFrom = new MailAddress(
387 $this->options->get( MainConfigNames::PasswordSender ),
388 $this->contLangMsgFormatter->format( MessageValue::new( 'emailsender' ) )
389 );
390 $replyTo = $fromAddress;
391 } else {
407 $mailFrom = $fromAddress;
408 $replyTo = null;
409 }
410 return [ $mailFrom, $replyTo ];
411 }
412
419 private function getSpecialMuteCanonicalURL( string $targetName ): string {
420 if ( defined( 'MW_PHPUNIT_TEST' ) ) {
421 return "Ceci n'est pas une URL";
422 }
423 return SpecialPage::getTitleFor( 'Mute', $targetName )->getCanonicalURL();
424 }
425
430 public function setEditToken( string $token ): void {
431 $this->editToken = $token;
432 }
433
434}
getUser()
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:88
Stores a single person's name and email address.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Command for sending emails to users.
Definition EmailUser.php:47
canSend()
Checks whether email sending is allowed.
sendEmailUnsafe(UserEmailContact $target, string $subject, string $text, bool $CCMe, string $langCode)
Really send a mail, without permission checks.
authorizeSend()
Authorize the email sending, checking permissions etc.
__construct(ServiceOptions $options, HookContainer $hookContainer, UserOptionsLookup $userOptionsLookup, CentralIdLookup $centralIdLookup, UserFactory $userFactory, IEmailer $emailer, IMessageFormatterFactory $messageFormatterFactory, ITextFormatter $contLangMsgFormatter, Authority $sender)
Definition EmailUser.php:93
validateTarget(UserEmailContact $target)
setEditToken(string $token)
A class containing constants representing the names of configuration variables.
const EnableUserEmail
Name constant for the EnableUserEmail setting, for use with Config::get()
const EnableSpecialMute
Name constant for the EnableSpecialMute setting, for use with Config::get()
const EnableEmail
Name constant for the EnableEmail setting, for use with Config::get()
const PasswordSender
Name constant for the PasswordSender setting, for use with Config::get()
const UserEmailUseReplyTo
Name constant for the UserEmailUseReplyTo setting, for use with Config::get()
A StatusValue for permission errors.
static splitIds( $str)
Splits a newline separated list of user ids into an array.
Parent class for all special pages.
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
Creates User objects.
Provides access to user options.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Value object representing a message for i18n.
Interface for sending emails.
Definition IEmailer.php:32
getUser()
Get the identity of the user this contact belongs to.
isEmailConfirmed()
Whether user email was confirmed.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
A simple factory providing a message formatter for a given language code.