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