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