@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Build separate mail for each recipient, honoring recipient access levels

Summary:
Ref T6367. Removes `multiplexMail()`!

We can't pass a single body into a function which splits it anymore: we need to split recipients first, then build bodies for each recipient list. This lets us build separate bodies for each recipient's individual translation/access levels.

The new logic does this:

- First, split recipients into groups called "targets".
- Each target corresponds to one actual mail we're going to build.
- Each target has a viewer (whose translation / access levels will be used to generate the mail).
- Each target has a to/cc list (the users who we'll ultimately send the mail to).
- For each target, build a custom mail body based on the viewer's access levels and settings (language prefs not actually implemented).
- Then, deliver the mail.

Test Plan:
- Read new config help.

Then did a bunch of testing, primarily with `bin/mail list-outbound` and `bin/mail show-outbound` (to review generated mail), `bin/phd debug taskmaster` (to run daemons freely) and `bin/worker execute --id <id>` (to repeatedly test a specific piece of code after identifying an issue).

With `one-mail-per-recipient` on (default):

- Sent mail to multiple users.
- Verified mail showed up in `mail list-outbound`.
- Examined mail with `mail show-outbound`.
- Added a project that a subscriber could not see.
- Verified it was not present in `X-Phabricator-Projects`.
- Verified it was rendered as "Restricted Project" for the non-permissioned viewer.
- Added a subscriber, then changed the object policy so they could not see it and sent mail.
- Verified I received mail but the other user did not.
- Enabled public replies and verified mail generated with public addresses.
- Disabld public replies and verified mail generated with private addresses.

With `one-mail-per-recipient` off:

- Verified that one mail is sent to all recipients.
- Verified users who can not see the object are still filtered.
- Verified that partially-visible projects are completely visible in the mail (this violates policies, as documented, as the best available compromise).
- Enabled public replies and verified the mail generated with "Reply To".

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: carlsverre, epriestley

Maniphest Tasks: T6367

Differential Revision: https://secure.phabricator.com/D13131

+408 -237
+2
src/__phutil_library_map__.php
··· 2021 2021 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 2022 2022 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 2023 2023 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 2024 + 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 2024 2025 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 2025 2026 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', 2026 2027 'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php', ··· 5429 5430 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 5430 5431 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 5431 5432 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 5433 + 'PhabricatorMailTarget' => 'Phobject', 5432 5434 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 5433 5435 'PhabricatorMainMenuSearchView' => 'AphrontView', 5434 5436 'PhabricatorMainMenuView' => 'AphrontView',
+4 -2
src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
··· 54 54 of each approach are: 55 55 56 56 - One mail to everyone: 57 + - This violates policy controls. The body of the mail is generated without 58 + respect for object policies. 57 59 - Recipients can see To/Cc at a glance. 58 60 - If you use mailing lists, you won't get duplicate mail if you're 59 61 a normal recipient and also Cc'd on a mailing list. ··· 65 67 - Not supported with a private reply-to address. 66 68 - Mails are sent in the server default translation. 67 69 - One mail to each user: 70 + - Policy controls work correctly and are enforced per-user. 68 71 - Recipients need to look in the mail body to see To/Cc. 69 72 - If you use mailing lists, recipients may sometimes get duplicate 70 73 mail. ··· 74 77 - Required if private reply-to addresses are configured. 75 78 - Mails are sent in the language of user preference. 76 79 77 - In the code, splitting one outbound email into one-per-recipient is sometimes 78 - referred to as "multiplexing". 79 80 EODOC 80 81 )); 81 82 ··· 222 223 'metamta.one-mail-per-recipient', 223 224 'bool', 224 225 true) 226 + ->setLocked(true) 225 227 ->setBoolOptions( 226 228 array( 227 229 pht('Send Mail To Each Recipient'),
+2 -3
src/applications/conpherence/mail/ConpherenceReplyHandler.php
··· 21 21 } 22 22 } 23 23 24 - public function getPrivateReplyHandlerEmailAddress( 25 - PhabricatorObjectHandle $handle) { 26 - return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'Z'); 24 + public function getPrivateReplyHandlerEmailAddress(PhabricatorUser $user) { 25 + return $this->getDefaultPrivateReplyHandlerEmailAddress($user, 'Z'); 27 26 } 28 27 29 28 public function getPublicReplyHandlerEmailAddress() {
+181 -175
src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
··· 47 47 48 48 abstract public function validateMailReceiver($mail_receiver); 49 49 abstract public function getPrivateReplyHandlerEmailAddress( 50 - PhabricatorObjectHandle $handle); 50 + PhabricatorUser $user); 51 51 52 52 public function getReplyHandlerDomain() { 53 53 return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); ··· 117 117 return null; 118 118 } 119 119 120 - final public function getRecipientsSummary( 121 - array $to_handles, 122 - array $cc_handles) { 123 - assert_instances_of($to_handles, 'PhabricatorObjectHandle'); 124 - assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); 125 - 126 - $body = ''; 127 - 128 - if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { 129 - if ($to_handles) { 130 - $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n"; 131 - } 132 - if ($cc_handles) { 133 - $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n"; 134 - } 135 - } 136 - 137 - return $body; 138 - } 139 - 140 - final public function getRecipientsSummaryHTML( 141 - array $to_handles, 142 - array $cc_handles) { 143 - assert_instances_of($to_handles, 'PhabricatorObjectHandle'); 144 - assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); 145 - 146 - if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { 147 - $body = array(); 148 - if ($to_handles) { 149 - $body[] = phutil_tag('strong', array(), 'To: '); 150 - $body[] = phutil_implode_html(', ', mpull($to_handles, 'getName')); 151 - $body[] = phutil_tag('br'); 152 - } 153 - if ($cc_handles) { 154 - $body[] = phutil_tag('strong', array(), 'Cc: '); 155 - $body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName')); 156 - $body[] = phutil_tag('br'); 157 - } 158 - return phutil_tag('div', array(), $body); 159 - } else { 160 - return ''; 161 - } 162 - 163 - } 164 - 165 - final public function multiplexMail( 166 - PhabricatorMetaMTAMail $mail_template, 167 - array $to_handles, 168 - array $cc_handles) { 169 - assert_instances_of($to_handles, 'PhabricatorObjectHandle'); 170 - assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); 171 - 172 - $result = array(); 173 - 174 - // If MetaMTA is configured to always multiplex, skip the single-email 175 - // case. 176 - if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { 177 - // If private replies are not supported, simply send one email to all 178 - // recipients and CCs. This covers cases where we have no reply handler, 179 - // or we have a public reply handler. 180 - if (!$this->supportsPrivateReplies()) { 181 - $mail = clone $mail_template; 182 - $mail->addTos(mpull($to_handles, 'getPHID')); 183 - $mail->addCCs(mpull($cc_handles, 'getPHID')); 184 - 185 - if ($this->supportsPublicReplies()) { 186 - $reply_to = $this->getPublicReplyHandlerEmailAddress(); 187 - $mail->setReplyTo($reply_to); 188 - } 189 - 190 - $result[] = $mail; 191 - 192 - return $result; 193 - } 194 - } 195 - 196 - // TODO: This is pretty messy. We should really be doing all of this 197 - // multiplexing in the task queue, but that requires significant rewriting 198 - // in the general case. ApplicationTransactions can do it fairly easily, 199 - // but other mail sites currently can not, so we need to support this 200 - // junky version until they catch up and we can swap things over. 201 - 202 - $to_handles = $this->expandRecipientHandles($to_handles); 203 - $cc_handles = $this->expandRecipientHandles($cc_handles); 204 - 205 - $tos = mpull($to_handles, null, 'getPHID'); 206 - $ccs = mpull($cc_handles, null, 'getPHID'); 207 - 208 - // Merge all the recipients together. TODO: We could keep the CCs as real 209 - // CCs and send to a "noreply@domain.com" type address, but keep it simple 210 - // for now. 211 - $recipients = $tos + $ccs; 212 - 213 - // When multiplexing mail, explicitly include To/Cc information in the 214 - // message body and headers. 215 - 216 - $mail_template = clone $mail_template; 217 - 218 - $mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos)); 219 - $mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs)); 220 - 221 - $body = $mail_template->getBody(); 222 - $body .= "\n"; 223 - $body .= $this->getRecipientsSummary($to_handles, $cc_handles); 224 - 225 - $html_body = $mail_template->getHTMLBody(); 226 - if (strlen($html_body)) { 227 - $html_body .= hsprintf('%s', 228 - $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); 229 - } 230 - 231 - foreach ($recipients as $phid => $recipient) { 232 - 233 - $mail = clone $mail_template; 234 - if (isset($to_handles[$phid])) { 235 - $mail->addTos(array($phid)); 236 - } else if (isset($cc_handles[$phid])) { 237 - $mail->addCCs(array($phid)); 238 - } else { 239 - // not good - they should be a to or a cc 240 - continue; 241 - } 242 - 243 - $mail->setBody($body); 244 - $mail->setHTMLBody($html_body); 245 - 246 - $reply_to = null; 247 - if (!$reply_to && $this->supportsPrivateReplies()) { 248 - $reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient); 249 - } 250 - 251 - if (!$reply_to && $this->supportsPublicReplies()) { 252 - $reply_to = $this->getPublicReplyHandlerEmailAddress(); 253 - } 254 - 255 - if ($reply_to) { 256 - $mail->setReplyTo($reply_to); 257 - } 258 - 259 - $result[] = $mail; 260 - } 261 - 262 - return $result; 263 - } 264 - 265 120 protected function getDefaultPublicReplyHandlerEmailAddress($prefix) { 266 121 267 122 $receiver = $this->getMailReceiver(); ··· 288 143 } 289 144 290 145 protected function getDefaultPrivateReplyHandlerEmailAddress( 291 - PhabricatorObjectHandle $handle, 146 + PhabricatorUser $user, 292 147 $prefix) { 293 148 294 - if ($handle->getType() != PhabricatorPeopleUserPHIDType::TYPECONST) { 295 - // You must be a real user to get a private reply handler address. 296 - return null; 297 - } 298 - 299 - $user = id(new PhabricatorPeopleQuery()) 300 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 301 - ->withPHIDs(array($handle->getPHID())) 302 - ->executeOne(); 303 - 304 - if (!$user) { 305 - // This may happen if a user was subscribed to something, and was then 306 - // deleted. 307 - return null; 308 - } 309 - 310 149 $receiver = $this->getMailReceiver(); 311 150 $receiver_id = $receiver->getID(); 312 151 $user_id = $user->getID(); 313 152 $hash = PhabricatorObjectMailReceiver::computeMailHash( 314 153 $receiver->getMailKey(), 315 - $handle->getPHID()); 154 + $user->getPHID()); 316 155 $domain = $this->getReplyHandlerDomain(); 317 156 318 157 $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}"; ··· 368 207 return rtrim($output); 369 208 } 370 209 371 - private function expandRecipientHandles(array $handles) { 372 - if (!$handles) { 210 + 211 + /** 212 + * Produce a list of mail targets for a given to/cc list. 213 + * 214 + * Each target should be sent a separate email, and contains the information 215 + * required to generate it with appropriate permissions and configuration. 216 + * 217 + * @param list<phid> List of "To" PHIDs. 218 + * @param list<phid> List of "CC" PHIDs. 219 + * @return list<PhabricatorMailTarget> List of targets. 220 + */ 221 + final public function getMailTargets(array $raw_to, array $raw_cc) { 222 + list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc); 223 + list($to, $cc) = $this->loadRecipientUsers($to, $cc); 224 + list($to, $cc) = $this->filterRecipientUsers($to, $cc); 225 + 226 + if (!$to && !$cc) { 373 227 return array(); 374 228 } 375 229 376 - $phids = mpull($handles, 'getPHID'); 377 - $results = id(new PhabricatorMetaMTAMemberQuery()) 378 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 379 - ->withPHIDs($phids) 380 - ->executeExpansion(); 230 + $template = id(new PhabricatorMailTarget()) 231 + ->setRawToPHIDs($raw_to) 232 + ->setRawCCPHIDs($raw_cc); 233 + 234 + // Set the public reply address as the default, if one exists. We 235 + // might replace this with a private address later. 236 + if ($this->supportsPublicReplies()) { 237 + $reply_to = $this->getPublicReplyHandlerEmailAddress(); 238 + if ($reply_to) { 239 + $template->setReplyTo($reply_to); 240 + } 241 + } 242 + 243 + $supports_private_replies = $this->supportsPrivateReplies(); 244 + $mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); 245 + $targets = array(); 246 + if ($mail_all) { 247 + $target = id(clone $template) 248 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 249 + ->setToMap($to) 250 + ->setCCMap($cc); 251 + 252 + $targets[] = $target; 253 + } else { 254 + $map = $to + $cc; 255 + 256 + foreach ($map as $phid => $user) { 257 + $target = id(clone $template) 258 + ->setViewer($user) 259 + ->setToMap(array($phid => $user)) 260 + ->setCCMap(array()); 261 + 262 + if ($supports_private_replies) { 263 + $reply_to = $this->getPrivateReplyHandlerEmailAddress($user); 264 + if ($reply_to) { 265 + $target->setReplyTo($reply_to); 266 + } 267 + } 381 268 382 - return id(new PhabricatorHandleQuery()) 383 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 384 - ->withPHIDs($results) 385 - ->execute(); 269 + $targets[] = $target; 270 + } 271 + } 272 + 273 + return $targets; 274 + } 275 + 276 + 277 + /** 278 + * Expand lists of recipient PHIDs. 279 + * 280 + * This takes any compound recipients (like projects) and looks up all their 281 + * members. 282 + * 283 + * @param list<phid> List of To PHIDs. 284 + * @param list<phid> List of CC PHIDs. 285 + * @return pair<list<phid>, list<phid>> Expanded PHID lists. 286 + */ 287 + private function expandRecipientPHIDs(array $to, array $cc) { 288 + $to_result = array(); 289 + $cc_result = array(); 290 + 291 + $all_phids = array_merge($to, $cc); 292 + if ($all_phids) { 293 + $map = id(new PhabricatorMetaMTAMemberQuery()) 294 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 295 + ->withPHIDs($all_phids) 296 + ->execute(); 297 + foreach ($to as $phid) { 298 + foreach ($map[$phid] as $expanded) { 299 + $to_result[$expanded] = $expanded; 300 + } 301 + } 302 + foreach ($cc as $phid) { 303 + foreach ($map[$phid] as $expanded) { 304 + $cc_result[$expanded] = $expanded; 305 + } 306 + } 307 + } 308 + 309 + // Remove recipients from "CC" if they're also present in "To". 310 + $cc_result = array_diff_key($cc_result, $to_result); 311 + 312 + return array(array_values($to_result), array_values($cc_result)); 313 + } 314 + 315 + 316 + /** 317 + * Load @{class:PhabricatorUser} objects for each recipient. 318 + * 319 + * Invalid recipients are dropped from the results. 320 + * 321 + * @param list<phid> List of To PHIDs. 322 + * @param list<phid> List of CC PHIDs. 323 + * @return pair<wild, wild> Maps from PHIDs to users. 324 + */ 325 + private function loadRecipientUsers(array $to, array $cc) { 326 + $to_result = array(); 327 + $cc_result = array(); 328 + 329 + $all_phids = array_merge($to, $cc); 330 + if ($all_phids) { 331 + $users = id(new PhabricatorPeopleQuery()) 332 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 333 + ->withPHIDs($all_phids) 334 + ->execute(); 335 + $users = mpull($users, null, 'getPHID'); 336 + 337 + foreach ($to as $phid) { 338 + if (isset($users[$phid])) { 339 + $to_result[$phid] = $users[$phid]; 340 + } 341 + } 342 + foreach ($cc as $phid) { 343 + if (isset($users[$phid])) { 344 + $cc_result[$phid] = $users[$phid]; 345 + } 346 + } 347 + } 348 + 349 + return array($to_result, $cc_result); 350 + } 351 + 352 + 353 + /** 354 + * Remove recipients who do not have permission to view the mail receiver. 355 + * 356 + * @param map<string, PhabricatorUser> Map of "To" users. 357 + * @param map<string, PhabricatorUser> Map of "CC" users. 358 + * @return pair<wild, wild> Filtered user maps. 359 + */ 360 + private function filterRecipientUsers(array $to, array $cc) { 361 + $to_result = array(); 362 + $cc_result = array(); 363 + 364 + $all_users = $to + $cc; 365 + if ($all_users) { 366 + $can_see = array(); 367 + $object = $this->getMailReceiver(); 368 + foreach ($all_users as $phid => $user) { 369 + $visible = PhabricatorPolicyFilter::hasCapability( 370 + $user, 371 + $object, 372 + PhabricatorPolicyCapability::CAN_VIEW); 373 + if ($visible) { 374 + $can_see[$phid] = true; 375 + } 376 + } 377 + 378 + foreach ($to as $phid => $user) { 379 + if (!empty($can_see[$phid])) { 380 + $to_result[$phid] = $all_users[$phid]; 381 + } 382 + } 383 + 384 + foreach ($cc as $phid => $user) { 385 + if (!empty($can_see[$phid])) { 386 + $cc_result[$phid] = $all_users[$phid]; 387 + } 388 + } 389 + } 390 + 391 + return array($to_result, $cc_result); 386 392 } 387 393 388 394 }
+146
src/applications/metamta/replyhandler/PhabricatorMailTarget.php
··· 1 + <?php 2 + 3 + final class PhabricatorMailTarget extends Phobject { 4 + 5 + private $viewer; 6 + private $replyTo; 7 + private $toMap = array(); 8 + private $ccMap = array(); 9 + private $rawToPHIDs; 10 + private $rawCCPHIDs; 11 + 12 + public function setRawToPHIDs(array $to_phids) { 13 + $this->rawToPHIDs = $to_phids; 14 + return $this; 15 + } 16 + 17 + public function setRawCCPHIDs(array $cc_phids) { 18 + $this->rawCCPHIDs = $cc_phids; 19 + return $this; 20 + } 21 + 22 + public function setCCMap(array $cc_map) { 23 + $this->ccMap = $cc_map; 24 + return $this; 25 + } 26 + 27 + public function getCCMap() { 28 + return $this->ccMap; 29 + } 30 + 31 + public function setToMap(array $to_map) { 32 + $this->toMap = $to_map; 33 + return $this; 34 + } 35 + 36 + public function getToMap() { 37 + return $this->toMap; 38 + } 39 + 40 + public function setReplyTo($reply_to) { 41 + $this->replyTo = $reply_to; 42 + return $this; 43 + } 44 + 45 + public function getReplyTo() { 46 + return $this->replyTo; 47 + } 48 + 49 + public function setViewer($viewer) { 50 + $this->viewer = $viewer; 51 + return $this; 52 + } 53 + 54 + public function getViewer() { 55 + return $this->viewer; 56 + } 57 + 58 + public function sendMail(PhabricatorMetaMTAMail $mail) { 59 + $viewer = $this->getViewer(); 60 + 61 + $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); 62 + $mail->addPHIDHeaders('X-Phabricator-Cc', $this->rawCCPHIDs); 63 + 64 + $to_handles = $viewer->loadHandles($this->rawToPHIDs); 65 + $cc_handles = $viewer->loadHandles($this->rawCCPHIDs); 66 + 67 + $body = $mail->getBody(); 68 + $body .= "\n"; 69 + $body .= $this->getRecipientsSummary($to_handles, $cc_handles); 70 + $mail->setBody($body); 71 + 72 + $html_body = $mail->getHTMLBody(); 73 + if (strlen($html_body)) { 74 + $html_body .= hsprintf( 75 + '%s', 76 + $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); 77 + } 78 + $mail->setHTMLBody($html_body); 79 + 80 + $reply_to = $this->getReplyTo(); 81 + if ($reply_to) { 82 + $mail->setReplyTo($reply_to); 83 + } 84 + 85 + $to = array_keys($this->getToMap()); 86 + if ($to) { 87 + $mail->addTos($to); 88 + } 89 + 90 + $cc = array_keys($this->getCCMap()); 91 + if ($cc) { 92 + $mail->addCCs($cc); 93 + } 94 + 95 + return $mail->save(); 96 + } 97 + 98 + private function getRecipientsSummary( 99 + PhabricatorHandleList $to_handles, 100 + PhabricatorHandleList $cc_handles) { 101 + 102 + if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { 103 + return ''; 104 + } 105 + 106 + $to_handles = iterator_to_array($to_handles); 107 + $cc_handles = iterator_to_array($cc_handles); 108 + 109 + $body = ''; 110 + if ($to_handles) { 111 + $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n"; 112 + } 113 + if ($cc_handles) { 114 + $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n"; 115 + } 116 + 117 + return $body; 118 + } 119 + 120 + private function getRecipientsSummaryHTML( 121 + PhabricatorHandleList $to_handles, 122 + PhabricatorHandleList $cc_handles) { 123 + 124 + if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { 125 + return ''; 126 + } 127 + 128 + $to_handles = iterator_to_array($to_handles); 129 + $cc_handles = iterator_to_array($cc_handles); 130 + 131 + $body = array(); 132 + if ($to_handles) { 133 + $body[] = phutil_tag('strong', array(), 'To: '); 134 + $body[] = phutil_implode_html(', ', mpull($to_handles, 'getName')); 135 + $body[] = phutil_tag('br'); 136 + } 137 + if ($cc_handles) { 138 + $body[] = phutil_tag('strong', array(), 'Cc: '); 139 + $body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName')); 140 + $body[] = phutil_tag('br'); 141 + } 142 + return phutil_tag('div', array(), $body); 143 + } 144 + 145 + 146 + }
+1 -1
src/applications/owners/mail/OwnersPackageReplyHandler.php
··· 11 11 } 12 12 13 13 public function getPrivateReplyHandlerEmailAddress( 14 - PhabricatorObjectHandle $handle) { 14 + PhabricatorUser $user) { 15 15 return null; 16 16 } 17 17
+1 -1
src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php
··· 8 8 } 9 9 10 10 public function getPrivateReplyHandlerEmailAddress( 11 - PhabricatorObjectHandle $handle) { 11 + PhabricatorUser $user) { 12 12 return null; 13 13 } 14 14
+18 -14
src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
··· 27 27 return; 28 28 } 29 29 30 + $targets = id(new PhabricatorRepositoryPushReplyHandler()) 31 + ->setMailReceiver($repository) 32 + ->getMailTargets($email_phids, array()); 33 + foreach ($targets as $target) { 34 + $this->sendMail($target, $repository, $event); 35 + } 36 + } 37 + 38 + private function sendMail( 39 + PhabricatorMailTarget $target, 40 + PhabricatorRepository $repository, 41 + PhabricatorRepositoryPushEvent $event) { 42 + 43 + $task_data = $this->getTaskData(); 44 + $viewer = $target->getViewer(); 45 + // TODO: Swap locale to viewer locale. 46 + 30 47 $logs = $event->getLogs(); 31 48 32 49 list($ref_lines, $ref_list) = $this->renderRefs($logs); ··· 103 120 ->addHeader('Thread-Topic', $subject) 104 121 ->setIsBulk(true); 105 122 106 - $to_handles = id(new PhabricatorHandleQuery()) 107 - ->setViewer($viewer) 108 - ->withPHIDs($email_phids) 109 - ->execute(); 110 - 111 - $reply_handler = new PhabricatorRepositoryPushReplyHandler(); 112 - $mails = $reply_handler->multiplexMail( 113 - $mail, 114 - $to_handles, 115 - array()); 116 - 117 - foreach ($mails as $mail) { 118 - $mail->saveAndSend(); 119 - } 123 + $target->sendMail($mail); 120 124 } 121 125 122 126 public function renderForDisplay(PhabricatorUser $viewer) {
+51 -39
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 992 992 // Hook for other edges that may need (re-)loading 993 993 $object = $this->willPublish($object, $xactions); 994 994 995 - $this->loadHandles($xactions); 996 - 997 - $mail = null; 995 + $mailed = array(); 998 996 if (!$this->getDisableEmail()) { 999 997 if ($this->shouldSendMail($object, $xactions)) { 1000 - $mail = $this->sendMail($object, $xactions); 998 + $mailed = $this->sendMail($object, $xactions); 1001 999 } 1002 1000 } 1003 1001 ··· 1009 1007 } 1010 1008 1011 1009 if ($this->shouldPublishFeedStory($object, $xactions)) { 1012 - $mailed = array(); 1013 - if ($mail) { 1014 - $mailed = $mail->buildRecipientList(); 1015 - } 1016 1010 $this->publishFeedStory( 1017 1011 $object, 1018 1012 $xactions, ··· 2128 2122 } 2129 2123 2130 2124 if (!$any_visible) { 2131 - return; 2125 + return array(); 2132 2126 } 2133 2127 2134 - $email_force = array(); 2135 2128 $email_to = $this->mailToPHIDs; 2136 2129 $email_cc = $this->mailCCPHIDs; 2137 - 2138 2130 $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); 2139 - $email_force = $this->heraldForcedEmailPHIDs; 2140 2131 2141 - $phids = array_merge($email_to, $email_cc); 2142 - $handles = id(new PhabricatorHandleQuery()) 2143 - ->setViewer($this->requireActor()) 2144 - ->withPHIDs($phids) 2145 - ->execute(); 2132 + $targets = $this->buildReplyHandler($object) 2133 + ->getMailTargets($email_to, $email_cc); 2146 2134 2147 - $template = $this->buildMailTemplate($object); 2135 + // Set this explicitly before we start swapping out the effective actor. 2136 + $this->setActingAsPHID($this->getActingAsPHID()); 2137 + 2138 + 2139 + $mailed = array(); 2140 + foreach ($targets as $target) { 2141 + $original_actor = $this->getActor(); 2142 + $this->setActor($target->getViewer()); 2143 + // TODO: Swap locale to viewer locale. 2144 + 2145 + $caught = null; 2146 + try { 2147 + // Reload handles for the new viewer. 2148 + $this->loadHandles($xactions); 2149 + 2150 + $mail = $this->sendMailToTarget($object, $xactions, $target); 2151 + } catch (Exception $ex) { 2152 + $caught = $ex; 2153 + } 2154 + 2155 + $this->setActor($original_actor); 2156 + if ($caught) { 2157 + throw $ex; 2158 + } 2159 + 2160 + foreach ($mail->buildRecipientList() as $phid) { 2161 + $mailed[$phid] = true; 2162 + } 2163 + } 2164 + 2165 + return array_keys($mailed); 2166 + } 2167 + 2168 + private function sendMailToTarget( 2169 + PhabricatorLiskDAO $object, 2170 + array $xactions, 2171 + PhabricatorMailTarget $target) { 2172 + 2173 + $mail = $this->buildMailTemplate($object); 2148 2174 $body = $this->buildMailBody($object, $xactions); 2149 2175 2150 2176 $mail_tags = $this->getMailTags($object, $xactions); 2151 2177 $action = $this->getMailAction($object, $xactions); 2152 - 2153 - $reply_handler = $this->buildReplyHandler($object); 2154 2178 2155 2179 if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { 2156 2180 $this->addEmailPreferenceSectionToMailBody( ··· 2159 2183 $xactions); 2160 2184 } 2161 2185 2162 - $template 2186 + $mail 2163 2187 ->setFrom($this->getActingAsPHID()) 2164 2188 ->setSubjectPrefix($this->getMailSubjectPrefix()) 2165 2189 ->setVarySubjectPrefix('['.$action.']') 2166 2190 ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) 2167 2191 ->setRelatedPHID($object->getPHID()) 2168 2192 ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) 2169 - ->setForceHeraldMailRecipientPHIDs($email_force) 2193 + ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs) 2170 2194 ->setMailTags($mail_tags) 2171 2195 ->setIsBulk(true) 2172 2196 ->setBody($body->render()) 2173 2197 ->setHTMLBody($body->renderHTML()); 2174 2198 2175 2199 foreach ($body->getAttachments() as $attachment) { 2176 - $template->addAttachment($attachment); 2200 + $mail->addAttachment($attachment); 2177 2201 } 2178 2202 2179 2203 if ($this->heraldHeader) { 2180 - $template->addHeader('X-Herald-Rules', $this->heraldHeader); 2204 + $mail->addHeader('X-Herald-Rules', $this->heraldHeader); 2181 2205 } 2182 2206 2183 2207 if ($object instanceof PhabricatorProjectInterface) { 2184 - $this->addMailProjectMetadata($object, $template); 2208 + $this->addMailProjectMetadata($object, $mail); 2185 2209 } 2186 2210 2187 2211 if ($this->getParentMessageID()) { 2188 - $template->setParentMessageID($this->getParentMessageID()); 2189 - } 2190 - 2191 - $mails = $reply_handler->multiplexMail( 2192 - $template, 2193 - array_select_keys($handles, $email_to), 2194 - array_select_keys($handles, $email_cc)); 2195 - 2196 - foreach ($mails as $mail) { 2197 - $mail->saveAndSend(); 2212 + $mail->setParentMessageID($this->getParentMessageID()); 2198 2213 } 2199 2214 2200 - $template->addTos($email_to); 2201 - $template->addCCs($email_cc); 2202 - 2203 - return $template; 2215 + return $target->sendMail($mail); 2204 2216 } 2205 2217 2206 2218 private function addMailProjectMetadata(
+2 -2
src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
··· 6 6 abstract public function getObjectPrefix(); 7 7 8 8 public function getPrivateReplyHandlerEmailAddress( 9 - PhabricatorObjectHandle $handle) { 9 + PhabricatorUser $user) { 10 10 return $this->getDefaultPrivateReplyHandlerEmailAddress( 11 - $handle, 11 + $user, 12 12 $this->getObjectPrefix()); 13 13 } 14 14