MediaWiki master
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
59 private ServiceOptions $options;
60
61 private HookRunner $hookRunner;
62
63 private UserOptionsLookup $userOptionsLookup;
64
65 private CentralIdLookup $centralIdLookup;
66
67 private UserFactory $userFactory;
68
69 private IEmailer $emailer;
70
71 private IMessageFormatterFactory $messageFormatterFactory;
72
73 private ITextFormatter $contLangMsgFormatter;
74
75 private Authority $sender;
76
78 private string $editToken = '';
79
91 public function __construct(
92 ServiceOptions $options,
93 HookContainer $hookContainer,
94 UserOptionsLookup $userOptionsLookup,
95 CentralIdLookup $centralIdLookup,
96 UserFactory $userFactory,
97 IEmailer $emailer,
98 IMessageFormatterFactory $messageFormatterFactory,
99 ITextFormatter $contLangMsgFormatter,
100 Authority $sender
101 ) {
102 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
103 $this->options = $options;
104 $this->hookRunner = new HookRunner( $hookContainer );
105 $this->userOptionsLookup = $userOptionsLookup;
106 $this->centralIdLookup = $centralIdLookup;
107 $this->userFactory = $userFactory;
108 $this->emailer = $emailer;
109 $this->messageFormatterFactory = $messageFormatterFactory;
110 $this->contLangMsgFormatter = $contLangMsgFormatter;
111
112 $this->sender = $sender;
113 }
114
122 public function validateTarget( UserEmailContact $target ): StatusValue {
123 $targetIdentity = $target->getUser();
124
125 if ( !$targetIdentity->getId() ) {
126 return StatusValue::newFatal( 'emailnotarget' );
127 }
128
129 if ( !$target->isEmailConfirmed() ) {
130 return StatusValue::newFatal( 'noemailtext' );
131 }
132
133 $targetUser = $this->userFactory->newFromUserIdentity( $targetIdentity );
134 if ( !$targetUser->canReceiveEmail() ) {
135 return StatusValue::newFatal( 'nowikiemailtext' );
136 }
137
138 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
139 if (
140 !$this->userOptionsLookup->getOption( $targetIdentity, 'email-allow-new-users' ) &&
141 $senderUser->isNewbie()
142 ) {
143 return StatusValue::newFatal( 'nowikiemailtext' );
144 }
145
146 $muteList = $this->userOptionsLookup->getOption(
147 $targetIdentity,
148 'email-blacklist',
149 ''
150 );
151 if ( $muteList ) {
152 $muteList = MultiUsernameFilter::splitIds( $muteList );
153 $senderId = $this->centralIdLookup->centralIdFromLocalUser( $this->sender->getUser() );
154 if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
155 return StatusValue::newFatal( 'nowikiemailtext' );
156 }
157 }
158
159 return StatusValue::newGood();
160 }
161
168 public function canSend(): StatusValue {
169 if (
170 !$this->options->get( MainConfigNames::EnableEmail ) ||
171 !$this->options->get( MainConfigNames::EnableUserEmail )
172 ) {
173 return StatusValue::newFatal( 'usermaildisabled' );
174 }
175
176 $user = $this->userFactory->newFromAuthority( $this->sender );
177
178 // Run this before checking 'sendemail' permission
179 // to show appropriate message to anons (T160309)
180 if ( !$user->isEmailConfirmed() ) {
181 return StatusValue::newFatal( 'mailnologin' );
182 }
183
184 $status = PermissionStatus::newGood();
185 if ( !$this->sender->isDefinitelyAllowed( 'sendemail', $status ) ) {
186 return $status;
187 }
188
189 $hookErr = false;
190
191 // TODO Remove deprecated hooks
192 $this->hookRunner->onUserCanSendEmail( $user, $hookErr );
193 $this->hookRunner->onEmailUserPermissionsErrors( $user, $this->editToken, $hookErr );
194 if ( is_array( $hookErr ) ) {
195 // SpamBlacklist uses null for the third element, and there might be more handlers not using an array.
196 $msgParamsArray = is_array( $hookErr[2] ) ? $hookErr[2] : [];
197 $ret = StatusValue::newFatal( $hookErr[1], ...$msgParamsArray );
198 $ret->value = $hookErr[0];
199 return $ret;
200 }
201
202 return StatusValue::newGood();
203 }
204
211 public function authorizeSend(): StatusValue {
212 $status = $this->canSend();
213 if ( !$status->isOK() ) {
214 return $status;
215 }
216
217 $status = PermissionStatus::newGood();
218 if ( !$this->sender->authorizeAction( 'sendemail', $status ) ) {
219 return $status;
220 }
221
222 $hookRes = $this->hookRunner->onEmailUserAuthorizeSend( $this->sender, $status );
223 if ( !$hookRes && !$status->isGood() ) {
224 return $status;
225 }
226
227 return StatusValue::newGood();
228 }
229
240 public function sendEmailUnsafe(
241 UserEmailContact $target,
242 string $subject,
243 string $text,
244 bool $CCMe,
245 string $langCode
246 ): StatusValue {
247 $senderIdentity = $this->sender->getUser();
248 $targetStatus = $this->validateTarget( $target );
249 if ( !$targetStatus->isGood() ) {
250 return $targetStatus;
251 }
252
253 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
254
255 $toAddress = MailAddress::newFromUser( $target );
256 $fromAddress = MailAddress::newFromUser( $senderUser );
257
258 // Add a standard footer and trim up trailing newlines
259 $text = rtrim( $text ) . "\n\n-- \n";
260 $text .= $this->contLangMsgFormatter->format(
261 MessageValue::new( 'emailuserfooter', [ $fromAddress->name, $toAddress->name ] )
262 );
263
264 if ( $this->options->get( MainConfigNames::EnableSpecialMute ) ) {
265 $text .= "\n" . $this->contLangMsgFormatter->format(
266 MessageValue::new(
267 'specialmute-email-footer',
268 [
269 $this->getSpecialMuteCanonicalURL( $senderIdentity->getName() ),
270 $senderIdentity->getName()
271 ]
272 )
273 );
274 }
275
276 $error = false;
277 // TODO Remove deprecated ugly hook
278 if ( !$this->hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
279 if ( $error instanceof StatusValue ) {
280 return $error;
281 } elseif ( $error === false || $error === '' || $error === [] ) {
282 // Possibly to tell HTMLForm to pretend there was no submission?
283 return StatusValue::newFatal( 'hookaborted' );
284 } elseif ( $error === true ) {
285 // Hook sent the mail itself and indicates success?
286 return StatusValue::newGood();
287 } elseif ( is_array( $error ) ) {
288 $status = StatusValue::newGood();
289 foreach ( $error as $e ) {
290 $status->fatal( $e );
291 }
292 return $status;
293 } elseif ( $error instanceof MessageSpecifier ) {
294 return StatusValue::newFatal( $error );
295 } else {
296 // Setting $error to something else was deprecated in 1.29 and
297 // removed in 1.36, and so an exception is now thrown
298 $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
299 throw new UnexpectedValueException(
300 'EmailUser hook set $error to unsupported type ' . $type
301 );
302 }
303 }
304
305 $hookStatus = StatusValue::newGood();
306 $hookRes = $this->hookRunner->onEmailUserSendEmail(
307 $this->sender,
308 $fromAddress,
309 $target,
310 $toAddress,
311 $subject,
312 $text,
313 $hookStatus
314 );
315 if ( !$hookRes && !$hookStatus->isGood() ) {
316 return $hookStatus;
317 }
318
319 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $fromAddress );
320
321 $status = $this->emailer->send(
322 $toAddress,
323 $mailFrom,
324 $subject,
325 $text,
326 null,
327 [ 'replyTo' => $replyTo ]
328 );
329
330 if ( !$status->isGood() ) {
331 return $status;
332 }
333
334 // if the user requested a copy of this mail, do this now,
335 // unless they are emailing themselves, in which case one
336 // copy of the message is sufficient.
337 if ( $CCMe && !$toAddress->equals( $fromAddress ) ) {
338 $userMsgFormatter = $this->messageFormatterFactory->getTextFormatter( $langCode );
339 $ccTo = $fromAddress;
340 $ccFrom = $fromAddress;
341 $ccSubject = $userMsgFormatter->format(
342 MessageValue::new( 'emailccsubject' )->plaintextParams(
343 $target->getUser()->getName(),
344 $subject
345 )
346 );
347 $ccText = $text;
348
349 $this->hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
350
351 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $ccFrom );
352
353 $ccStatus = $this->emailer->send(
354 $ccTo,
355 $mailFrom,
356 $ccSubject,
357 $ccText,
358 null,
359 [ 'replyTo' => $replyTo ]
360 );
361 $status->merge( $ccStatus );
362 }
363
364 $this->hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
365
366 return $status;
367 }
368
374 private function getFromAndReplyTo( MailAddress $fromAddress ): array {
375 if ( $this->options->get( MainConfigNames::UserEmailUseReplyTo ) ) {
384 $mailFrom = new MailAddress(
385 $this->options->get( MainConfigNames::PasswordSender ),
386 $this->contLangMsgFormatter->format( MessageValue::new( 'emailsender' ) )
387 );
388 $replyTo = $fromAddress;
389 } else {
405 $mailFrom = $fromAddress;
406 $replyTo = null;
407 }
408 return [ $mailFrom, $replyTo ];
409 }
410
417 private function getSpecialMuteCanonicalURL( string $targetName ): string {
418 if ( defined( 'MW_PHPUNIT_TEST' ) ) {
419 return "Ceci n'est pas une URL";
420 }
421 return SpecialPage::getTitleFor( 'Mute', $targetName )->getCanonicalURL();
422 }
423
428 public function setEditToken( string $token ): void {
429 $this->editToken = $token;
430 }
431
432}
getUser()
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
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:91
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.
Provides access to user options.
Creates User objects.
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 with the current execution context,...
Definition Authority.php:37
A simple factory providing a message formatter for a given language code.