MediaWiki master
EmailUser.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Mail;
8
20use StatusValue;
21use UnexpectedValueException;
26
37class EmailUser {
41 public const CONSTRUCTOR_OPTIONS = [
46 ];
47
48 private ServiceOptions $options;
49 private HookRunner $hookRunner;
50 private UserOptionsLookup $userOptionsLookup;
51 private CentralIdLookup $centralIdLookup;
52 private UserFactory $userFactory;
53 private IEmailer $emailer;
54 private IMessageFormatterFactory $messageFormatterFactory;
55 private ITextFormatter $contLangMsgFormatter;
56 private Authority $sender;
57
59 private string $editToken = '';
60
74 public function __construct(
75 ServiceOptions $options,
76 HookContainer $hookContainer,
77 UserOptionsLookup $userOptionsLookup,
78 CentralIdLookup $centralIdLookup,
79 UserFactory $userFactory,
80 IEmailer $emailer,
81 IMessageFormatterFactory $messageFormatterFactory,
82 ITextFormatter $contLangMsgFormatter,
83 Authority $sender
84 ) {
85 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
86 $this->options = $options;
87 $this->hookRunner = new HookRunner( $hookContainer );
88 $this->userOptionsLookup = $userOptionsLookup;
89 $this->centralIdLookup = $centralIdLookup;
90 $this->userFactory = $userFactory;
91 $this->emailer = $emailer;
92 $this->messageFormatterFactory = $messageFormatterFactory;
93 $this->contLangMsgFormatter = $contLangMsgFormatter;
94
95 $this->sender = $sender;
96 }
97
105 public function validateTarget( UserEmailContact $target ): StatusValue {
106 $targetIdentity = $target->getUser();
107
108 if ( !$targetIdentity->getId() ) {
109 return StatusValue::newFatal( 'emailnotarget' );
110 }
111
112 if ( !$target->isEmailConfirmed() ) {
113 return StatusValue::newFatal( 'noemailtext' );
114 }
115
116 $targetUser = $this->userFactory->newFromUserIdentity( $targetIdentity );
117 if ( !$targetUser->canReceiveEmail() ) {
118 return StatusValue::newFatal( 'nowikiemailtext' );
119 }
120
121 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
122 if (
123 !$this->userOptionsLookup->getOption( $targetIdentity, 'email-allow-new-users' ) &&
124 $senderUser->isNewbie()
125 ) {
126 return StatusValue::newFatal( 'nowikiemailtext' );
127 }
128
129 $muteList = $this->userOptionsLookup->getOption(
130 $targetIdentity,
131 'email-blacklist',
132 ''
133 );
134 if ( $muteList ) {
135 $muteList = MultiUsernameFilter::splitIds( $muteList );
136 $senderId = $this->centralIdLookup->centralIdFromLocalUser( $this->sender->getUser() );
137 if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
138 return StatusValue::newFatal( 'nowikiemailtext' );
139 }
140 }
141
142 return StatusValue::newGood();
143 }
144
151 public function canSend(): StatusValue {
152 if (
153 !$this->options->get( MainConfigNames::EnableEmail ) ||
154 !$this->options->get( MainConfigNames::EnableUserEmail )
155 ) {
156 return StatusValue::newFatal( 'usermaildisabled' );
157 }
158
159 $user = $this->userFactory->newFromAuthority( $this->sender );
160
161 // Run this before checking 'sendemail' permission
162 // to show appropriate message to anons (T160309)
163 if ( !$user->isEmailConfirmed() ) {
164 return StatusValue::newFatal( 'mailnologin' );
165 }
166
167 $status = PermissionStatus::newGood();
168 if ( !$this->sender->isDefinitelyAllowed( 'sendemail', $status ) ) {
169 return $status;
170 }
171
172 $hookErr = false;
173
174 // TODO Remove deprecated hooks
175 $this->hookRunner->onUserCanSendEmail( $user, $hookErr );
176 $this->hookRunner->onEmailUserPermissionsErrors( $user, $this->editToken, $hookErr );
177 if ( is_array( $hookErr ) ) {
178 // SpamBlacklist uses null for the third element, and there might be more handlers not using an array.
179 $msgParamsArray = is_array( $hookErr[2] ) ? $hookErr[2] : [];
180 $ret = StatusValue::newFatal( $hookErr[1], ...$msgParamsArray );
181 $ret->value = $hookErr[0];
182 return $ret;
183 }
184
185 return StatusValue::newGood();
186 }
187
194 public function authorizeSend(): StatusValue {
195 $status = $this->canSend();
196 if ( !$status->isOK() ) {
197 return $status;
198 }
199
200 $status = PermissionStatus::newGood();
201 if ( !$this->sender->authorizeAction( 'sendemail', $status ) ) {
202 return $status;
203 }
204
205 $hookRes = $this->hookRunner->onEmailUserAuthorizeSend( $this->sender, $status );
206 if ( !$hookRes && !$status->isGood() ) {
207 return $status;
208 }
209
210 return StatusValue::newGood();
211 }
212
223 public function sendEmailUnsafe(
224 UserEmailContact $target,
225 string $subject,
226 string $text,
227 bool $CCMe,
228 string $langCode
229 ): StatusValue {
230 $senderIdentity = $this->sender->getUser();
231 $targetStatus = $this->validateTarget( $target );
232 if ( !$targetStatus->isGood() ) {
233 return $targetStatus;
234 }
235
236 $senderUser = $this->userFactory->newFromAuthority( $this->sender );
237
238 $toAddress = MailAddress::newFromUser( $target );
239 $fromAddress = MailAddress::newFromUser( $senderUser );
240
241 // Add a standard footer and trim up trailing newlines
242 $text = rtrim( $text ) . "\n\n-- \n";
243 $text .= $this->contLangMsgFormatter->format(
244 MessageValue::new( 'emailuserfooter', [ $fromAddress->name, $toAddress->name ] )
245 );
246
247 $text .= "\n" . $this->contLangMsgFormatter->format(
248 MessageValue::new(
249 'specialmute-email-footer',
250 [
251 $this->getSpecialMuteCanonicalURL( $senderIdentity->getName() ),
252 $senderIdentity->getName()
253 ]
254 )
255 );
256
257 $error = false;
258 // TODO Remove deprecated ugly hook
259 if ( !$this->hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
260 if ( $error instanceof StatusValue ) {
261 return $error;
262 } elseif ( $error === false || $error === '' || $error === [] ) {
263 // Possibly to tell HTMLForm to pretend there was no submission?
264 return StatusValue::newFatal( 'hookaborted' );
265 } elseif ( $error === true ) {
266 // Hook sent the mail itself and indicates success?
267 return StatusValue::newGood();
268 } elseif ( is_array( $error ) ) {
269 $status = StatusValue::newGood();
270 foreach ( $error as $e ) {
271 $status->fatal( $e );
272 }
273 return $status;
274 } elseif ( $error instanceof MessageSpecifier ) {
275 return StatusValue::newFatal( $error );
276 } else {
277 // Setting $error to something else was deprecated in 1.29 and
278 // removed in 1.36, and so an exception is now thrown
279 $type = get_debug_type( $error );
280 throw new UnexpectedValueException(
281 'EmailUser hook set $error to unsupported type ' . $type
282 );
283 }
284 }
285
286 $hookStatus = StatusValue::newGood();
287 $hookRes = $this->hookRunner->onEmailUserSendEmail(
288 $this->sender,
289 $fromAddress,
290 $target,
291 $toAddress,
292 $subject,
293 $text,
294 $hookStatus
295 );
296 if ( !$hookRes && !$hookStatus->isGood() ) {
297 return $hookStatus;
298 }
299
300 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $fromAddress );
301
302 $status = $this->emailer->send(
303 $toAddress,
304 $mailFrom,
305 $subject,
306 $text,
307 null,
308 [ 'replyTo' => $replyTo ]
309 );
310
311 if ( !$status->isGood() ) {
312 return $status;
313 }
314
315 // if the user requested a copy of this mail, do this now,
316 // unless they are emailing themselves, in which case one
317 // copy of the message is sufficient.
318 if ( $CCMe && !$toAddress->equals( $fromAddress ) ) {
319 $userMsgFormatter = $this->messageFormatterFactory->getTextFormatter( $langCode );
320 $ccTo = $fromAddress;
321 $ccFrom = $fromAddress;
322 $ccSubject = $userMsgFormatter->format(
323 MessageValue::new( 'emailccsubject' )->plaintextParams(
324 $target->getUser()->getName(),
325 $subject
326 )
327 );
328 $ccText = $text;
329
330 $this->hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
331
332 [ $mailFrom, $replyTo ] = $this->getFromAndReplyTo( $ccFrom );
333
334 $ccStatus = $this->emailer->send(
335 $ccTo,
336 $mailFrom,
337 $ccSubject,
338 $ccText,
339 null,
340 [ 'replyTo' => $replyTo ]
341 );
342 $status->merge( $ccStatus );
343 }
344
345 $this->hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
346
347 return $status;
348 }
349
355 private function getFromAndReplyTo( MailAddress $fromAddress ): array {
356 if ( $this->options->get( MainConfigNames::UserEmailUseReplyTo ) ) {
365 $mailFrom = new MailAddress(
366 $this->options->get( MainConfigNames::PasswordSender ),
367 $this->contLangMsgFormatter->format( MessageValue::new( 'emailsender' ) )
368 );
369 $replyTo = $fromAddress;
370 } else {
386 $mailFrom = $fromAddress;
387 $replyTo = null;
388 }
389 return [ $mailFrom, $replyTo ];
390 }
391
398 private function getSpecialMuteCanonicalURL( string $targetName ): string {
399 if ( defined( 'MW_PHPUNIT_TEST' ) ) {
400 return "Ceci n'est pas une URL";
401 }
402 return SpecialPage::getTitleFor( 'Mute', $targetName )->getCanonicalURL();
403 }
404
409 public function setEditToken( string $token ): void {
410 $this->editToken = $token;
411 }
412
413}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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:37
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:74
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 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.
Find central user IDs associated with local user IDs, e.g.
Provides access to user options.
Create 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:20
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:23
A simple factory providing a message formatter for a given language code.