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