@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.

Refactor mail to produce an intermediate "bag of strings" object in preparation for SMS

Summary:
Depends on D19954. Ref T920. This is a step toward a world where "Mailers" are generic and may send messages over a broader array of channels (email, SMS, postal mail).

There are a few major parts here:

- Instead of calling `$mailer->setSubject()`, `$mailer->setFrom()`, etc., build in intermediate `$message` object first, then pass that to the mailer.
- This breaks every mailer! This change on its own does not fix them. I plan to fix them in a series of "update mailer X", "update mailer Y" followups.
- This generally makes the API easier to change in the far future, and in the near future supports mailers accepting different types of `$message` objects with the same API.
- Pull the "build an email" stuff out into a `PhabricatorMailEmailEngine`. `MetaMTAMail` is already a huge object without also doing this translation step. This is just a separation/simplification change, but also tries to fight against `MetaMTAMail` getting 5K lines of email/sms/whatsapp/postal-mail code.
- Try to rewrite the "build an email" stuff to be a bit more straightforward while making it generate objects. Prior to this change, it had this weird flow:

```lang=php
foreach ($properties as $key => $prop) {
switch ($key) {
case 'xyz':
// ...
}
}
```

This is just inherently somewhat hard to puzzle out, and it means that processing order depends on internal property order, which is quite surprising.

Test Plan: This breaks everything on its own; adapters must be updated to use the new API. See followups.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T920

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

+813 -544
+4
src/__phutil_library_map__.php
··· 3389 3389 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', 3390 3390 'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php', 3391 3391 'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php', 3392 + 'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php', 3392 3393 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 3393 3394 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 3394 3395 'PhabricatorMailEmailMessage' => 'applications/metamta/message/PhabricatorMailEmailMessage.php', ··· 3414 3415 'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php', 3415 3416 'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php', 3416 3417 'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php', 3418 + 'PhabricatorMailMessageEngine' => 'applications/metamta/engine/PhabricatorMailMessageEngine.php', 3417 3419 'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php', 3418 3420 'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php', 3419 3421 'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php', ··· 9221 9223 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 9222 9224 'PhabricatorMailAttachment' => 'Phobject', 9223 9225 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 9226 + 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', 9224 9227 'PhabricatorMailEmailHeraldField' => 'HeraldField', 9225 9228 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 9226 9229 'PhabricatorMailEmailMessage' => 'PhabricatorMailExternalMessage', ··· 9246 9249 'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow', 9247 9250 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 9248 9251 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 9252 + 'PhabricatorMailMessageEngine' => 'Phobject', 9249 9253 'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction', 9250 9254 'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter', 9251 9255 'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction',
-3
src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
··· 187 187 ->setStacked(true); 188 188 189 189 $headers = $mail->getDeliveredHeaders(); 190 - if ($headers === null) { 191 - $headers = $mail->generateHeaders(); 192 - } 193 190 194 191 // Sort headers by name. 195 192 $headers = isort($headers, 0);
+649
src/applications/metamta/engine/PhabricatorMailEmailEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorMailEmailEngine 4 + extends PhabricatorMailMessageEngine { 5 + 6 + public function newMessage() { 7 + $mailer = $this->getMailer(); 8 + $mail = $this->getMail(); 9 + 10 + $message = new PhabricatorMailEmailMessage(); 11 + 12 + $from_address = $this->newFromEmailAddress(); 13 + $message->setFromAddress($from_address); 14 + 15 + $reply_address = $this->newReplyToEmailAddress(); 16 + if ($reply_address) { 17 + $message->setReplyToAddress($reply_address); 18 + } 19 + 20 + $to_addresses = $this->newToEmailAddresses(); 21 + $cc_addresses = $this->newCCEmailAddresses(); 22 + 23 + if (!$to_addresses && !$cc_addresses) { 24 + $mail->setMessage( 25 + pht( 26 + 'Message has no valid recipients: all To/CC are disabled, '. 27 + 'invalid, or configured not to receive this mail.')); 28 + return null; 29 + } 30 + 31 + // If this email describes a mail processing error, we rate limit outbound 32 + // messages to each individual address. This prevents messes where 33 + // something is stuck in a loop or dumps a ton of messages on us suddenly. 34 + if ($mail->getIsErrorEmail()) { 35 + $all_recipients = array(); 36 + foreach ($to_addresses as $to_address) { 37 + $all_recipients[] = $to_address->getAddress(); 38 + } 39 + foreach ($cc_addresses as $cc_address) { 40 + $all_recipients[] = $cc_address->getAddress(); 41 + } 42 + if ($this->shouldRateLimitMail($all_recipients)) { 43 + $mail->setMessage( 44 + pht( 45 + 'This is an error email, but one or more recipients have '. 46 + 'exceeded the error email rate limit. Declining to deliver '. 47 + 'message.')); 48 + return null; 49 + } 50 + } 51 + 52 + // Some mailers require a valid "To:" in order to deliver mail. If we 53 + // don't have any "To:", try to fill it in with a placeholder "To:". 54 + // If that also fails, move the "Cc:" line to "To:". 55 + if (!$to_addresses) { 56 + $void_address = $this->newVoidEmailAddress(); 57 + $cc_addresses = $to_addresses; 58 + $to_addresses = array($void_address); 59 + } 60 + 61 + $to_addresses = $this->getUniqueEmailAddresses($to_addresses); 62 + $cc_addresses = $this->getUniqueEmailAddresses( 63 + $cc_addresses, 64 + $to_addresses); 65 + 66 + $message->setToAddresses($to_addresses); 67 + $message->setCCAddresses($cc_addresses); 68 + 69 + $attachments = $this->newEmailAttachments(); 70 + $message->setAttachments($attachments); 71 + 72 + $subject = $this->newEmailSubject(); 73 + $message->setSubject($subject); 74 + 75 + $headers = $this->newEmailHeaders(); 76 + foreach ($this->newEmailThreadingHeaders($mailer) as $threading_header) { 77 + $headers[] = $threading_header; 78 + } 79 + 80 + $stamps = $mail->getMailStamps(); 81 + if ($stamps) { 82 + $headers[] = $this->newEmailHeader( 83 + 'X-Phabricator-Stamps', 84 + implode(' ', $stamps)); 85 + } 86 + 87 + $must_encrypt = $mail->getMustEncrypt(); 88 + 89 + $raw_body = $mail->getBody(); 90 + $body = $raw_body; 91 + if ($must_encrypt) { 92 + $parts = array(); 93 + 94 + $encrypt_uri = $this->getMustEncryptURI(); 95 + if (!strlen($encrypt_uri)) { 96 + $encrypt_phid = $this->getRelatedPHID(); 97 + if ($encrypt_phid) { 98 + $encrypt_uri = urisprintf( 99 + '/object/%s/', 100 + $encrypt_phid); 101 + } 102 + } 103 + 104 + if (strlen($encrypt_uri)) { 105 + $parts[] = pht( 106 + 'This secure message is notifying you of a change to this object:'); 107 + $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri); 108 + } 109 + 110 + $parts[] = pht( 111 + 'The content for this message can only be transmitted over a '. 112 + 'secure channel. To view the message content, follow this '. 113 + 'link:'); 114 + 115 + $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); 116 + 117 + $body = implode("\n\n", $parts); 118 + } else { 119 + $body = $raw_body; 120 + } 121 + 122 + $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); 123 + if (strlen($body) > $body_limit) { 124 + $body = id(new PhutilUTF8StringTruncator()) 125 + ->setMaximumBytes($body_limit) 126 + ->truncateString($body); 127 + $body .= "\n"; 128 + $body .= pht('(This email was truncated at %d bytes.)', $body_limit); 129 + } 130 + $message->setTextBody($body); 131 + $body_limit -= strlen($body); 132 + 133 + // If we sent a different message body than we were asked to, record 134 + // what we actually sent to make debugging and diagnostics easier. 135 + if ($body !== $raw_body) { 136 + $mail->setDeliveredBody($body); 137 + } 138 + 139 + if ($must_encrypt) { 140 + $send_html = false; 141 + } else { 142 + $send_html = $this->shouldSendHTML(); 143 + } 144 + 145 + if ($send_html) { 146 + $html_body = $mail->getHTMLBody(); 147 + if (strlen($html_body)) { 148 + // NOTE: We just drop the entire HTML body if it won't fit. Safely 149 + // truncating HTML is hard, and we already have the text body to fall 150 + // back to. 151 + if (strlen($html_body) <= $body_limit) { 152 + $message->setHTMLBody($html_body); 153 + $body_limit -= strlen($html_body); 154 + } 155 + } 156 + } 157 + 158 + // Pass the headers to the mailer, then save the state so we can show 159 + // them in the web UI. If the mail must be encrypted, we remove headers 160 + // which are not on a strict whitelist to avoid disclosing information. 161 + $filtered_headers = $this->filterHeaders($headers, $must_encrypt); 162 + $message->setHeaders($filtered_headers); 163 + 164 + $mail->setUnfilteredHeaders($headers); 165 + $mail->setDeliveredHeaders($headers); 166 + 167 + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { 168 + $mail->setMessage( 169 + pht( 170 + 'Phabricator is running in silent mode. See `%s` '. 171 + 'in the configuration to change this setting.', 172 + 'phabricator.silent')); 173 + 174 + return null; 175 + } 176 + 177 + return $message; 178 + } 179 + 180 + /* -( Message Components )------------------------------------------------- */ 181 + 182 + private function newFromEmailAddress() { 183 + $from_address = $this->newDefaultEmailAddress(); 184 + $mail = $this->getMail(); 185 + 186 + // If the mail content must be encrypted, always disguise the sender. 187 + $must_encrypt = $mail->getMustEncrypt(); 188 + if ($must_encrypt) { 189 + return $from_address; 190 + } 191 + 192 + // If we have a raw "From" address, use that. 193 + $raw_from = $mail->getRawFrom(); 194 + if ($raw_from) { 195 + list($from_email, $from_name) = $raw_from; 196 + return $this->newEmailAddress($from_email, $from_name); 197 + } 198 + 199 + // Otherwise, use as much of the information for any sending entity as 200 + // we can. 201 + $from_phid = $mail->getFrom(); 202 + 203 + $actor = $this->getActor($from_phid); 204 + if ($actor) { 205 + $actor_email = $actor->getEmailAddress(); 206 + $actor_name = $actor->getName(); 207 + } else { 208 + $actor_email = null; 209 + $actor_name = null; 210 + } 211 + 212 + $send_as_user = PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); 213 + if ($send_as_user) { 214 + if ($actor_email !== null) { 215 + $from_address->setAddress($actor_email); 216 + } 217 + } 218 + 219 + if ($actor_name !== null) { 220 + $from_address->setDisplayName($actor_name); 221 + } 222 + 223 + return $from_address; 224 + } 225 + 226 + private function newReplyToEmailAddress() { 227 + $mail = $this->getMail(); 228 + 229 + $reply_raw = $mail->getReplyTo(); 230 + if (!strlen($reply_raw)) { 231 + return null; 232 + } 233 + 234 + $reply_address = new PhutilEmailAddress($reply_raw); 235 + 236 + // If we have a sending object, change the display name. 237 + $from_phid = $mail->getFrom(); 238 + $actor = $this->getActor($from_phid); 239 + if ($actor) { 240 + $reply_address->setDisplayName($actor->getName()); 241 + } 242 + 243 + // If we don't have a display name, fill in a default. 244 + if (!strlen($reply_address->getDisplayName())) { 245 + $reply_address->setDisplayName(pht('Phabricator')); 246 + } 247 + 248 + return $reply_address; 249 + } 250 + 251 + private function newToEmailAddresses() { 252 + $mail = $this->getMail(); 253 + 254 + $phids = $mail->getToPHIDs(); 255 + $addresses = $this->newEmailAddressesFromActorPHIDs($phids); 256 + 257 + foreach ($mail->getRawToAddresses() as $raw_address) { 258 + $addresses[] = new PhutilEmailAddress($raw_address); 259 + } 260 + 261 + return $addresses; 262 + } 263 + 264 + private function newCCEmailAddresses() { 265 + $mail = $this->getMail(); 266 + $phids = $mail->getCcPHIDs(); 267 + return $this->newEmailAddressesFromActorPHIDs($phids); 268 + } 269 + 270 + private function newEmailAddressesFromActorPHIDs(array $phids) { 271 + $mail = $this->getMail(); 272 + $phids = $mail->expandRecipients($phids); 273 + 274 + $addresses = array(); 275 + foreach ($phids as $phid) { 276 + $actor = $this->getActor($phid); 277 + if (!$actor) { 278 + continue; 279 + } 280 + 281 + if (!$actor->isDeliverable()) { 282 + continue; 283 + } 284 + 285 + $addresses[] = new PhutilEmailAddress($actor->getEmailAddress()); 286 + } 287 + 288 + return $addresses; 289 + } 290 + 291 + private function newEmailSubject() { 292 + $mail = $this->getMail(); 293 + 294 + $is_threaded = (bool)$mail->getThreadID(); 295 + $must_encrypt = $mail->getMustEncrypt(); 296 + 297 + $subject = array(); 298 + 299 + if ($is_threaded) { 300 + if ($this->shouldAddRePrefix()) { 301 + $subject[] = 'Re:'; 302 + } 303 + } 304 + 305 + $subject[] = trim($mail->getSubjectPrefix()); 306 + 307 + // If mail content must be encrypted, we replace the subject with 308 + // a generic one. 309 + if ($must_encrypt) { 310 + $encrypt_subject = $mail->getMustEncryptSubject(); 311 + if (!strlen($encrypt_subject)) { 312 + $encrypt_subject = pht('Object Updated'); 313 + } 314 + $subject[] = $encrypt_subject; 315 + } else { 316 + $vary_prefix = $mail->getVarySubjectPrefix(); 317 + if (strlen($vary_prefix)) { 318 + if ($this->shouldVarySubject()) { 319 + $subject[] = $vary_prefix; 320 + } 321 + } 322 + 323 + $subject[] = $mail->getSubject(); 324 + } 325 + 326 + foreach ($subject as $key => $part) { 327 + if (!strlen($part)) { 328 + unset($subject[$key]); 329 + } 330 + } 331 + 332 + $subject = implode(' ', $subject); 333 + return $subject; 334 + } 335 + 336 + private function newEmailHeaders() { 337 + $mail = $this->getMail(); 338 + 339 + $headers = array(); 340 + 341 + $headers[] = $this->newEmailHeader( 342 + 'X-Phabricator-Sent-This-Message', 343 + 'Yes'); 344 + $headers[] = $this->newEmailHeader( 345 + 'X-Mail-Transport-Agent', 346 + 'MetaMTA'); 347 + 348 + // Some clients respect this to suppress OOF and other auto-responses. 349 + $headers[] = $this->newEmailHeader( 350 + 'X-Auto-Response-Suppress', 351 + 'All'); 352 + 353 + $mailtags = $mail->getMailTags(); 354 + if ($mailtags) { 355 + $tag_header = array(); 356 + foreach ($mailtags as $mailtag) { 357 + $tag_header[] = '<'.$mailtag.'>'; 358 + } 359 + $tag_header = implode(', ', $tag_header); 360 + $headers[] = $this->newEmailHeader( 361 + 'X-Phabricator-Mail-Tags', 362 + $tag_header); 363 + } 364 + 365 + $value = $mail->getHeaders(); 366 + foreach ($value as $pair) { 367 + list($header_key, $header_value) = $pair; 368 + 369 + // NOTE: If we have \n in a header, SES rejects the email. 370 + $header_value = str_replace("\n", ' ', $header_value); 371 + $headers[] = $this->newEmailHeader($header_key, $header_value); 372 + } 373 + 374 + $is_bulk = $mail->getIsBulk(); 375 + if ($is_bulk) { 376 + $headers[] = $this->newEmailHeader('Precedence', 'bulk'); 377 + } 378 + 379 + if ($mail->getMustEncrypt()) { 380 + $headers[] = $this->newEmailHeader('X-Phabricator-Must-Encrypt', 'Yes'); 381 + } 382 + 383 + $related_phid = $mail->getRelatedPHID(); 384 + if ($related_phid) { 385 + $headers[] = $this->newEmailHeader('Thread-Topic', $related_phid); 386 + } 387 + 388 + $headers[] = $this->newEmailHeader( 389 + 'X-Phabricator-Mail-ID', 390 + $mail->getID()); 391 + 392 + $unique = Filesystem::readRandomCharacters(16); 393 + $headers[] = $this->newEmailHeader( 394 + 'X-Phabricator-Send-Attempt', 395 + $unique); 396 + 397 + return $headers; 398 + } 399 + 400 + private function newEmailThreadingHeaders() { 401 + $mailer = $this->getMailer(); 402 + $mail = $this->getMail(); 403 + 404 + $headers = array(); 405 + 406 + $thread_id = $mail->getThreadID(); 407 + if (!strlen($thread_id)) { 408 + return $headers; 409 + } 410 + 411 + $is_first = $mail->getIsFirstMessage(); 412 + 413 + // NOTE: Gmail freaks out about In-Reply-To and References which aren't in 414 + // the form "<string@domain.tld>"; this is also required by RFC 2822, 415 + // although some clients are more liberal in what they accept. 416 + $domain = $this->newMailDomain(); 417 + $thread_id = '<'.$thread_id.'@'.$domain.'>'; 418 + 419 + if ($is_first && $mailer->supportsMessageIDHeader()) { 420 + $headers[] = $this->newEmailHeader('Message-ID', $thread_id); 421 + } else { 422 + $in_reply_to = $thread_id; 423 + $references = array($thread_id); 424 + $parent_id = $mail->getParentMessageID(); 425 + if ($parent_id) { 426 + $in_reply_to = $parent_id; 427 + // By RFC 2822, the most immediate parent should appear last 428 + // in the "References" header, so this order is intentional. 429 + $references[] = $parent_id; 430 + } 431 + $references = implode(' ', $references); 432 + $headers[] = $this->newEmailHeader('In-Reply-To', $in_reply_to); 433 + $headers[] = $this->newEmailHeader('References', $references); 434 + } 435 + $thread_index = $this->generateThreadIndex($thread_id, $is_first); 436 + $headers[] = $this->newEmailHeader('Thread-Index', $thread_index); 437 + 438 + return $headers; 439 + } 440 + 441 + private function newEmailAttachments() { 442 + $mail = $this->getMail(); 443 + 444 + // If the mail content must be encrypted, don't add attachments. 445 + $must_encrypt = $mail->getMustEncrypt(); 446 + if ($must_encrypt) { 447 + return array(); 448 + } 449 + 450 + return $mail->getAttachments(); 451 + } 452 + 453 + /* -( Preferences )-------------------------------------------------------- */ 454 + 455 + private function shouldAddRePrefix() { 456 + $preferences = $this->getPreferences(); 457 + 458 + $value = $preferences->getSettingValue( 459 + PhabricatorEmailRePrefixSetting::SETTINGKEY); 460 + 461 + return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX); 462 + } 463 + 464 + private function shouldVarySubject() { 465 + $preferences = $this->getPreferences(); 466 + 467 + $value = $preferences->getSettingValue( 468 + PhabricatorEmailVarySubjectsSetting::SETTINGKEY); 469 + 470 + return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS); 471 + } 472 + 473 + private function shouldSendHTML() { 474 + $preferences = $this->getPreferences(); 475 + 476 + $value = $preferences->getSettingValue( 477 + PhabricatorEmailFormatSetting::SETTINGKEY); 478 + 479 + return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); 480 + } 481 + 482 + 483 + /* -( Utilities )---------------------------------------------------------- */ 484 + 485 + private function newEmailHeader($name, $value) { 486 + return id(new PhabricatorMailHeader()) 487 + ->setName($name) 488 + ->setValue($value); 489 + } 490 + 491 + private function newEmailAddress($address, $name = null) { 492 + $object = id(new PhutilEmailAddress()) 493 + ->setAddress($address); 494 + 495 + if (strlen($name)) { 496 + $object->setDisplayName($name); 497 + } 498 + 499 + return $object; 500 + } 501 + 502 + public function newDefaultEmailAddress() { 503 + $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address'); 504 + 505 + if (!strlen($raw_address)) { 506 + $domain = $this->newMailDomain(); 507 + $raw_address = "noreply@{$domain}"; 508 + } 509 + 510 + $address = new PhutilEmailAddress($raw_address); 511 + 512 + if (!strlen($address->getDisplayName())) { 513 + $address->setDisplayName(pht('Phabricator')); 514 + } 515 + 516 + return $address; 517 + } 518 + 519 + public function newVoidEmailAddress() { 520 + return $this->newDefaultEmailAddress(); 521 + } 522 + 523 + private function newMailDomain() { 524 + $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); 525 + if (strlen($domain)) { 526 + return $domain; 527 + } 528 + 529 + $install_uri = PhabricatorEnv::getURI('/'); 530 + $install_uri = new PhutilURI($install_uri); 531 + 532 + return $install_uri->getDomain(); 533 + } 534 + 535 + private function filterHeaders(array $headers, $must_encrypt) { 536 + assert_instances_of($headers, 'PhabricatorMailHeader'); 537 + 538 + if (!$must_encrypt) { 539 + return $headers; 540 + } 541 + 542 + $whitelist = array( 543 + 'In-Reply-To', 544 + 'Message-ID', 545 + 'Precedence', 546 + 'References', 547 + 'Thread-Index', 548 + 'Thread-Topic', 549 + 550 + 'X-Mail-Transport-Agent', 551 + 'X-Auto-Response-Suppress', 552 + 553 + 'X-Phabricator-Sent-This-Message', 554 + 'X-Phabricator-Must-Encrypt', 555 + 'X-Phabricator-Mail-ID', 556 + 'X-Phabricator-Send-Attempt', 557 + ); 558 + 559 + // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". 560 + // This header contains a significant amount of meaningful information 561 + // about the object. 562 + 563 + $whitelist_map = array(); 564 + foreach ($whitelist as $term) { 565 + $whitelist_map[phutil_utf8_strtolower($term)] = true; 566 + } 567 + 568 + foreach ($headers as $key => $header) { 569 + $name = $header->getName(); 570 + $name = phutil_utf8_strtolower($name); 571 + 572 + if (!isset($whitelist_map[$name])) { 573 + unset($headers[$key]); 574 + } 575 + } 576 + 577 + return $headers; 578 + } 579 + 580 + private function getUniqueEmailAddresses( 581 + array $addresses, 582 + array $exclude = array()) { 583 + assert_instances_of($addresses, 'PhutilEmailAddress'); 584 + assert_instances_of($exclude, 'PhutilEmailAddress'); 585 + 586 + $seen = array(); 587 + 588 + foreach ($exclude as $address) { 589 + $seen[$address->getAddress()] = true; 590 + } 591 + 592 + foreach ($addresses as $key => $address) { 593 + $raw_address = $address->getAddress(); 594 + 595 + if (isset($seen[$raw_address])) { 596 + unset($addresses[$key]); 597 + continue; 598 + } 599 + 600 + $seen[$raw_address] = true; 601 + } 602 + 603 + return array_values($addresses); 604 + } 605 + 606 + private function generateThreadIndex($seed, $is_first_mail) { 607 + // When threading, Outlook ignores the 'References' and 'In-Reply-To' 608 + // headers that most clients use. Instead, it uses a custom 'Thread-Index' 609 + // header. The format of this header is something like this (from 610 + // camel-exchange-folder.c in Evolution Exchange): 611 + 612 + /* A new post to a folder gets a 27-byte-long thread index. (The value 613 + * is apparently unique but meaningless.) Each reply to a post gets a 614 + * 32-byte-long thread index whose first 27 bytes are the same as the 615 + * parent's thread index. Each reply to any of those gets a 616 + * 37-byte-long thread index, etc. The Thread-Index header contains a 617 + * base64 representation of this value. 618 + */ 619 + 620 + // The specific implementation uses a 27-byte header for the first email 621 + // a recipient receives, and a random 5-byte suffix (32 bytes total) 622 + // thereafter. This means that all the replies are (incorrectly) siblings, 623 + // but it would be very difficult to keep track of the entire tree and this 624 + // gets us reasonable client behavior. 625 + 626 + $base = substr(md5($seed), 0, 27); 627 + if (!$is_first_mail) { 628 + // Not totally sure, but it seems like outlook orders replies by 629 + // thread-index rather than timestamp, so to get these to show up in the 630 + // right order we use the time as the last 4 bytes. 631 + $base .= ' '.pack('N', time()); 632 + } 633 + 634 + return base64_encode($base); 635 + } 636 + 637 + private function shouldRateLimitMail(array $all_recipients) { 638 + try { 639 + PhabricatorSystemActionEngine::willTakeAction( 640 + $all_recipients, 641 + new PhabricatorMetaMTAErrorMailAction(), 642 + 1); 643 + return false; 644 + } catch (PhabricatorSystemActionRateLimitException $ex) { 645 + return true; 646 + } 647 + } 648 + 649 + }
+55
src/applications/metamta/engine/PhabricatorMailMessageEngine.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorMailMessageEngine 4 + extends Phobject { 5 + 6 + private $mailer; 7 + private $mail; 8 + private $actors = array(); 9 + private $preferences; 10 + 11 + final public function setMailer( 12 + PhabricatorMailImplementationAdapter $mailer) { 13 + 14 + $this->mailer = $mailer; 15 + return $this; 16 + } 17 + 18 + final public function getMailer() { 19 + return $this->mailer; 20 + } 21 + 22 + final public function setMail(PhabricatorMetaMTAMail $mail) { 23 + $this->mail = $mail; 24 + return $this; 25 + } 26 + 27 + final public function getMail() { 28 + return $this->mail; 29 + } 30 + 31 + final public function setActors(array $actors) { 32 + assert_instances_of($actors, 'PhabricatorMetaMTAActor'); 33 + $this->actors = $actors; 34 + return $this; 35 + } 36 + 37 + final public function getActors() { 38 + return $this->actors; 39 + } 40 + 41 + final public function getActor($phid) { 42 + return idx($this->actors, $phid); 43 + } 44 + 45 + final public function setPreferences( 46 + PhabricatorUserPreferences $preferences) { 47 + $this->preferences = $preferences; 48 + return $this; 49 + } 50 + 51 + final public function getPreferences() { 52 + return $this->preferences; 53 + } 54 + 55 + }
-4
src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
··· 116 116 117 117 $headers = $message->getDeliveredHeaders(); 118 118 $unfiltered = $message->getUnfilteredHeaders(); 119 - if (!$unfiltered) { 120 - $headers = $message->generateHeaders(); 121 - $unfiltered = $headers; 122 - } 123 119 124 120 $header_map = array(); 125 121 foreach ($headers as $header) {
+105 -537
src/applications/metamta/storage/PhabricatorMetaMTAMail.php
··· 191 191 return $this; 192 192 } 193 193 194 + public function getHeaders() { 195 + return $this->getParam('headers', array()); 196 + } 197 + 194 198 public function addAttachment(PhabricatorMailAttachment $attachment) { 195 199 $this->parameters['attachments'][] = $attachment->toDictionary(); 196 200 return $this; 197 201 } 198 202 199 203 public function getAttachments() { 200 - $dicts = $this->getParam('attachments'); 204 + $dicts = $this->getParam('attachments', array()); 201 205 202 206 $result = array(); 203 207 foreach ($dicts as $dict) { ··· 256 260 return $this; 257 261 } 258 262 263 + public function getRawFrom() { 264 + return $this->getParam('raw-from'); 265 + } 266 + 259 267 public function setReplyTo($reply_to) { 260 268 $this->setParam('reply-to', $reply_to); 261 269 return $this; 270 + } 271 + 272 + public function getReplyTo() { 273 + return $this->getParam('reply-to'); 262 274 } 263 275 264 276 public function setSubject($subject) { ··· 271 283 return $this; 272 284 } 273 285 286 + public function getSubjectPrefix() { 287 + return $this->getParam('subject-prefix'); 288 + } 289 + 274 290 public function setVarySubjectPrefix($prefix) { 275 291 $this->setParam('vary-subject-prefix', $prefix); 276 292 return $this; 277 293 } 278 294 295 + public function getVarySubjectPrefix() { 296 + return $this->getParam('vary-subject-prefix'); 297 + } 298 + 279 299 public function setBody($body) { 280 300 $this->setParam('body', $body); 281 301 return $this; ··· 413 433 return $this; 414 434 } 415 435 436 + public function getIsBulk() { 437 + return $this->getParam('is-bulk'); 438 + } 439 + 416 440 /** 417 441 * Use this method to set an ID used for message threading. MetaMTA will 418 442 * set appropriate headers (Message-ID, In-Reply-To, References and ··· 429 453 return $this; 430 454 } 431 455 456 + public function getThreadID() { 457 + return $this->getParam('thread-id'); 458 + } 459 + 460 + public function getIsFirstMessage() { 461 + return (bool)$this->getParam('is-first-message'); 462 + } 463 + 432 464 /** 433 465 * Save a newly created mail to the database. The mail will eventually be 434 466 * delivered by the MetaMTA daemon. ··· 597 629 } 598 630 } 599 631 600 - foreach ($sorted as $mailer) { 601 - $mailer->prepareForSend(); 602 - } 603 - 604 632 return $sorted; 605 633 } 606 634 ··· 627 655 ->save(); 628 656 } 629 657 630 - $exceptions = array(); 631 - foreach ($mailers as $template_mailer) { 632 - $mailer = null; 658 + $actors = $this->loadAllActors(); 659 + 660 + // If we're sending one mail to everyone, some recipients will be in 661 + // "Cc" rather than "To". We'll move them to "To" later (or supply a 662 + // dummy "To") but need to look for the recipient in either the 663 + // "To" or "Cc" fields here. 664 + $target_phid = head($this->getToPHIDs()); 665 + if (!$target_phid) { 666 + $target_phid = head($this->getCcPHIDs()); 667 + } 668 + $preferences = $this->loadPreferences($target_phid); 633 669 670 + // Attach any files we're about to send to this message, so the recipients 671 + // can view them. 672 + $viewer = PhabricatorUser::getOmnipotentUser(); 673 + $files = $this->loadAttachedFiles($viewer); 674 + foreach ($files as $file) { 675 + $file->attachToObject($this->getPHID()); 676 + } 677 + 678 + $exceptions = array(); 679 + foreach ($mailers as $mailer) { 634 680 try { 635 - $mailer = $this->buildMailer($template_mailer); 681 + $message = id(new PhabricatorMailEmailEngine()) 682 + ->setMailer($mailer) 683 + ->setMail($this) 684 + ->setActors($actors) 685 + ->setPreferences($preferences) 686 + ->newMessage($mailer); 636 687 } catch (Exception $ex) { 637 688 $exceptions[] = $ex; 638 689 continue; 639 690 } 640 691 641 - if (!$mailer) { 642 - // If we don't get a mailer back, that means the mail doesn't 643 - // actually need to be sent (for example, because recipients have 644 - // declined to receive the mail). Void it and return. 692 + if (!$message) { 693 + // If we don't get a message back, that means the mail doesn't actually 694 + // need to be sent (for example, because recipients have declined to 695 + // receive the mail). Void it and return. 645 696 return $this 646 697 ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) 647 698 ->save(); 648 699 } 649 700 650 701 try { 651 - $ok = $mailer->send(); 652 - if (!$ok) { 653 - // TODO: At some point, we should clean this up and make all mailers 654 - // throw. 655 - throw new Exception( 656 - pht( 657 - 'Mail adapter encountered an unexpected, unspecified '. 658 - 'failure.')); 659 - } 702 + $mailer->sendMessage($message); 660 703 } catch (PhabricatorMetaMTAPermanentFailureException $ex) { 661 704 // If any mailer raises a permanent failure, stop trying to send the 662 705 // mail with other mailers. ··· 677 720 $this->setParam('mailer.key', $mailer_key); 678 721 } 679 722 723 + // Now that we sent the message, store the final deliverability outcomes 724 + // and reasoning so we can explain why things happened the way they did. 725 + $actor_list = array(); 726 + foreach ($actors as $actor) { 727 + $actor_list[$actor->getPHID()] = array( 728 + 'deliverable' => $actor->isDeliverable(), 729 + 'reasons' => $actor->getDeliverabilityReasons(), 730 + ); 731 + } 732 + $this->setParam('actors.sent', $actor_list); 733 + $this->setParam('routing.sent', $this->getParam('routing')); 734 + $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); 735 + 680 736 return $this 681 737 ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT) 682 738 ->save(); ··· 705 761 $exceptions); 706 762 } 707 763 708 - private function buildMailer(PhabricatorMailImplementationAdapter $mailer) { 709 - $headers = $this->generateHeaders(); 710 - 711 - $params = $this->parameters; 712 - 713 - $actors = $this->loadAllActors(); 714 - $deliverable_actors = $this->filterDeliverableActors($actors); 715 - 716 - $default_from = (string)$this->newDefaultEmailAddress(); 717 - if (empty($params['from'])) { 718 - $mailer->setFrom($default_from); 719 - } 720 - 721 - $is_first = idx($params, 'is-first-message'); 722 - unset($params['is-first-message']); 723 - 724 - $is_threaded = (bool)idx($params, 'thread-id'); 725 - $must_encrypt = $this->getMustEncrypt(); 726 - 727 - $reply_to_name = idx($params, 'reply-to-name', ''); 728 - unset($params['reply-to-name']); 729 - 730 - $add_cc = array(); 731 - $add_to = array(); 732 - 733 - // If we're sending one mail to everyone, some recipients will be in 734 - // "Cc" rather than "To". We'll move them to "To" later (or supply a 735 - // dummy "To") but need to look for the recipient in either the 736 - // "To" or "Cc" fields here. 737 - $target_phid = head(idx($params, 'to', array())); 738 - if (!$target_phid) { 739 - $target_phid = head(idx($params, 'cc', array())); 740 - } 741 - 742 - $preferences = $this->loadPreferences($target_phid); 743 - 744 - foreach ($params as $key => $value) { 745 - switch ($key) { 746 - case 'raw-from': 747 - list($from_email, $from_name) = $value; 748 - $mailer->setFrom($from_email, $from_name); 749 - break; 750 - case 'from': 751 - // If the mail content must be encrypted, disguise the sender. 752 - if ($must_encrypt) { 753 - $mailer->setFrom($default_from, pht('Phabricator')); 754 - break; 755 - } 756 - 757 - $from = $value; 758 - $actor_email = null; 759 - $actor_name = null; 760 - $actor = idx($actors, $from); 761 - if ($actor) { 762 - $actor_email = $actor->getEmailAddress(); 763 - $actor_name = $actor->getName(); 764 - } 765 - $can_send_as_user = $actor_email && 766 - PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); 767 - 768 - if ($can_send_as_user) { 769 - $mailer->setFrom($actor_email, $actor_name); 770 - } else { 771 - $from_email = coalesce($actor_email, $default_from); 772 - $from_name = coalesce($actor_name, pht('Phabricator')); 773 - 774 - if (empty($params['reply-to'])) { 775 - $params['reply-to'] = $from_email; 776 - $params['reply-to-name'] = $from_name; 777 - } 778 - 779 - $mailer->setFrom($default_from, $from_name); 780 - } 781 - break; 782 - case 'reply-to': 783 - $mailer->addReplyTo($value, $reply_to_name); 784 - break; 785 - case 'to': 786 - $to_phids = $this->expandRecipients($value); 787 - $to_actors = array_select_keys($deliverable_actors, $to_phids); 788 - $add_to = array_merge( 789 - $add_to, 790 - mpull($to_actors, 'getEmailAddress')); 791 - break; 792 - case 'raw-to': 793 - $add_to = array_merge($add_to, $value); 794 - break; 795 - case 'cc': 796 - $cc_phids = $this->expandRecipients($value); 797 - $cc_actors = array_select_keys($deliverable_actors, $cc_phids); 798 - $add_cc = array_merge( 799 - $add_cc, 800 - mpull($cc_actors, 'getEmailAddress')); 801 - break; 802 - case 'attachments': 803 - $attached_viewer = PhabricatorUser::getOmnipotentUser(); 804 - $files = $this->loadAttachedFiles($attached_viewer); 805 - foreach ($files as $file) { 806 - $file->attachToObject($this->getPHID()); 807 - } 808 - 809 - // If the mail content must be encrypted, don't add attachments. 810 - if ($must_encrypt) { 811 - break; 812 - } 813 - 814 - $value = $this->getAttachments(); 815 - foreach ($value as $attachment) { 816 - $mailer->addAttachment( 817 - $attachment->getData(), 818 - $attachment->getFilename(), 819 - $attachment->getMimeType()); 820 - } 821 - break; 822 - case 'subject': 823 - $subject = array(); 824 - 825 - if ($is_threaded) { 826 - if ($this->shouldAddRePrefix($preferences)) { 827 - $subject[] = 'Re:'; 828 - } 829 - } 830 - 831 - $subject[] = trim(idx($params, 'subject-prefix')); 832 - 833 - // If mail content must be encrypted, we replace the subject with 834 - // a generic one. 835 - if ($must_encrypt) { 836 - $encrypt_subject = $this->getMustEncryptSubject(); 837 - if (!strlen($encrypt_subject)) { 838 - $encrypt_subject = pht('Object Updated'); 839 - } 840 - $subject[] = $encrypt_subject; 841 - } else { 842 - $vary_prefix = idx($params, 'vary-subject-prefix'); 843 - if ($vary_prefix != '') { 844 - if ($this->shouldVarySubject($preferences)) { 845 - $subject[] = $vary_prefix; 846 - } 847 - } 848 - 849 - $subject[] = $value; 850 - } 851 - 852 - $mailer->setSubject(implode(' ', array_filter($subject))); 853 - break; 854 - case 'thread-id': 855 - 856 - // NOTE: Gmail freaks out about In-Reply-To and References which 857 - // aren't in the form "<string@domain.tld>"; this is also required 858 - // by RFC 2822, although some clients are more liberal in what they 859 - // accept. 860 - $domain = $this->newMailDomain(); 861 - $value = '<'.$value.'@'.$domain.'>'; 862 - 863 - if ($is_first && $mailer->supportsMessageIDHeader()) { 864 - $headers[] = array('Message-ID', $value); 865 - } else { 866 - $in_reply_to = $value; 867 - $references = array($value); 868 - $parent_id = $this->getParentMessageID(); 869 - if ($parent_id) { 870 - $in_reply_to = $parent_id; 871 - // By RFC 2822, the most immediate parent should appear last 872 - // in the "References" header, so this order is intentional. 873 - $references[] = $parent_id; 874 - } 875 - $references = implode(' ', $references); 876 - $headers[] = array('In-Reply-To', $in_reply_to); 877 - $headers[] = array('References', $references); 878 - } 879 - $thread_index = $this->generateThreadIndex($value, $is_first); 880 - $headers[] = array('Thread-Index', $thread_index); 881 - break; 882 - default: 883 - // Other parameters are handled elsewhere or are not relevant to 884 - // constructing the message. 885 - break; 886 - } 887 - } 888 - 889 - $stamps = $this->getMailStamps(); 890 - if ($stamps) { 891 - $headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps)); 892 - } 893 - 894 - $raw_body = idx($params, 'body', ''); 895 - $body = $raw_body; 896 - if ($must_encrypt) { 897 - $parts = array(); 898 - 899 - $encrypt_uri = $this->getMustEncryptURI(); 900 - if (!strlen($encrypt_uri)) { 901 - $encrypt_phid = $this->getRelatedPHID(); 902 - if ($encrypt_phid) { 903 - $encrypt_uri = urisprintf( 904 - '/object/%s/', 905 - $encrypt_phid); 906 - } 907 - } 908 - 909 - if (strlen($encrypt_uri)) { 910 - $parts[] = pht( 911 - 'This secure message is notifying you of a change to this object:'); 912 - $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri); 913 - } 914 - 915 - $parts[] = pht( 916 - 'The content for this message can only be transmitted over a '. 917 - 'secure channel. To view the message content, follow this '. 918 - 'link:'); 919 - 920 - $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); 921 - 922 - $body = implode("\n\n", $parts); 923 - } else { 924 - $body = $raw_body; 925 - } 926 - 927 - $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); 928 - if (strlen($body) > $body_limit) { 929 - $body = id(new PhutilUTF8StringTruncator()) 930 - ->setMaximumBytes($body_limit) 931 - ->truncateString($body); 932 - $body .= "\n"; 933 - $body .= pht('(This email was truncated at %d bytes.)', $body_limit); 934 - } 935 - $mailer->setBody($body); 936 - $body_limit -= strlen($body); 937 - 938 - // If we sent a different message body than we were asked to, record 939 - // what we actually sent to make debugging and diagnostics easier. 940 - if ($body !== $raw_body) { 941 - $this->setParam('body.sent', $body); 942 - } 943 - 944 - if ($must_encrypt) { 945 - $send_html = false; 946 - } else { 947 - $send_html = $this->shouldSendHTML($preferences); 948 - } 949 - 950 - if ($send_html) { 951 - $html_body = idx($params, 'html-body'); 952 - if (strlen($html_body)) { 953 - // NOTE: We just drop the entire HTML body if it won't fit. Safely 954 - // truncating HTML is hard, and we already have the text body to fall 955 - // back to. 956 - if (strlen($html_body) <= $body_limit) { 957 - $mailer->setHTMLBody($html_body); 958 - $body_limit -= strlen($html_body); 959 - } 960 - } 961 - } 962 - 963 - // Pass the headers to the mailer, then save the state so we can show 964 - // them in the web UI. If the mail must be encrypted, we remove headers 965 - // which are not on a strict whitelist to avoid disclosing information. 966 - $filtered_headers = $this->filterHeaders($headers, $must_encrypt); 967 - foreach ($filtered_headers as $header) { 968 - list($header_key, $header_value) = $header; 969 - $mailer->addHeader($header_key, $header_value); 970 - } 971 - $this->setParam('headers.unfiltered', $headers); 972 - $this->setParam('headers.sent', $filtered_headers); 973 - 974 - // Save the final deliverability outcomes and reasoning so we can 975 - // explain why things happened the way they did. 976 - $actor_list = array(); 977 - foreach ($actors as $actor) { 978 - $actor_list[$actor->getPHID()] = array( 979 - 'deliverable' => $actor->isDeliverable(), 980 - 'reasons' => $actor->getDeliverabilityReasons(), 981 - ); 982 - } 983 - $this->setParam('actors.sent', $actor_list); 984 - 985 - $this->setParam('routing.sent', $this->getParam('routing')); 986 - $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); 987 - 988 - if (!$add_to && !$add_cc) { 989 - $this->setMessage( 990 - pht( 991 - 'Message has no valid recipients: all To/Cc are disabled, '. 992 - 'invalid, or configured not to receive this mail.')); 993 - 994 - return null; 995 - } 996 - 997 - if ($this->getIsErrorEmail()) { 998 - $all_recipients = array_merge($add_to, $add_cc); 999 - if ($this->shouldRateLimitMail($all_recipients)) { 1000 - $this->setMessage( 1001 - pht( 1002 - 'This is an error email, but one or more recipients have '. 1003 - 'exceeded the error email rate limit. Declining to deliver '. 1004 - 'message.')); 1005 - 1006 - return null; 1007 - } 1008 - } 1009 - 1010 - if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { 1011 - $this->setMessage( 1012 - pht( 1013 - 'Phabricator is running in silent mode. See `%s` '. 1014 - 'in the configuration to change this setting.', 1015 - 'phabricator.silent')); 1016 - 1017 - return null; 1018 - } 1019 - 1020 - // Some mailers require a valid "To:" in order to deliver mail. If we don't 1021 - // have any "To:", fill it in with a placeholder "To:". This allows client 1022 - // rules based on whether the recipient is in "To:" or "CC:" to continue 1023 - // behaving in the same way. 1024 - if (!$add_to) { 1025 - $void_recipient = $this->newVoidEmailAddress(); 1026 - $add_to = array($void_recipient->getAddress()); 1027 - } 1028 - 1029 - $add_to = array_unique($add_to); 1030 - $add_cc = array_diff(array_unique($add_cc), $add_to); 1031 - 1032 - $mailer->addTos($add_to); 1033 - if ($add_cc) { 1034 - $mailer->addCCs($add_cc); 1035 - } 1036 - 1037 - return $mailer; 1038 - } 1039 - 1040 - private function generateThreadIndex($seed, $is_first_mail) { 1041 - // When threading, Outlook ignores the 'References' and 'In-Reply-To' 1042 - // headers that most clients use. Instead, it uses a custom 'Thread-Index' 1043 - // header. The format of this header is something like this (from 1044 - // camel-exchange-folder.c in Evolution Exchange): 1045 - 1046 - /* A new post to a folder gets a 27-byte-long thread index. (The value 1047 - * is apparently unique but meaningless.) Each reply to a post gets a 1048 - * 32-byte-long thread index whose first 27 bytes are the same as the 1049 - * parent's thread index. Each reply to any of those gets a 1050 - * 37-byte-long thread index, etc. The Thread-Index header contains a 1051 - * base64 representation of this value. 1052 - */ 1053 - 1054 - // The specific implementation uses a 27-byte header for the first email 1055 - // a recipient receives, and a random 5-byte suffix (32 bytes total) 1056 - // thereafter. This means that all the replies are (incorrectly) siblings, 1057 - // but it would be very difficult to keep track of the entire tree and this 1058 - // gets us reasonable client behavior. 1059 - 1060 - $base = substr(md5($seed), 0, 27); 1061 - if (!$is_first_mail) { 1062 - // Not totally sure, but it seems like outlook orders replies by 1063 - // thread-index rather than timestamp, so to get these to show up in the 1064 - // right order we use the time as the last 4 bytes. 1065 - $base .= ' '.pack('N', time()); 1066 - } 1067 - 1068 - return base64_encode($base); 1069 - } 1070 764 1071 765 public static function shouldMailEachRecipient() { 1072 766 return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); ··· 1120 814 * recipients. 1121 815 * @return list<phid> Deaggregated list of mailable recipients. 1122 816 */ 1123 - private function expandRecipients(array $phids) { 817 + public function expandRecipients(array $phids) { 1124 818 if ($this->recipientExpansionMap === null) { 1125 819 $all_phids = $this->getAllActorPHIDs(); 1126 820 $this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery()) ··· 1320 1014 return $actors; 1321 1015 } 1322 1016 1323 - private function shouldRateLimitMail(array $all_recipients) { 1324 - try { 1325 - PhabricatorSystemActionEngine::willTakeAction( 1326 - $all_recipients, 1327 - new PhabricatorMetaMTAErrorMailAction(), 1328 - 1); 1329 - return false; 1330 - } catch (PhabricatorSystemActionRateLimitException $ex) { 1331 - return true; 1332 - } 1017 + public function getDeliveredHeaders() { 1018 + return $this->getParam('headers.sent'); 1333 1019 } 1334 1020 1335 - public function generateHeaders() { 1336 - $headers = array(); 1337 - 1338 - $headers[] = array('X-Phabricator-Sent-This-Message', 'Yes'); 1339 - $headers[] = array('X-Mail-Transport-Agent', 'MetaMTA'); 1340 - 1341 - // Some clients respect this to suppress OOF and other auto-responses. 1342 - $headers[] = array('X-Auto-Response-Suppress', 'All'); 1343 - 1344 - $mailtags = $this->getParam('mailtags'); 1345 - if ($mailtags) { 1346 - $tag_header = array(); 1347 - foreach ($mailtags as $mailtag) { 1348 - $tag_header[] = '<'.$mailtag.'>'; 1349 - } 1350 - $tag_header = implode(', ', $tag_header); 1351 - $headers[] = array('X-Phabricator-Mail-Tags', $tag_header); 1352 - } 1353 - 1354 - $value = $this->getParam('headers', array()); 1355 - foreach ($value as $pair) { 1356 - list($header_key, $header_value) = $pair; 1357 - 1358 - // NOTE: If we have \n in a header, SES rejects the email. 1359 - $header_value = str_replace("\n", ' ', $header_value); 1360 - $headers[] = array($header_key, $header_value); 1361 - } 1362 - 1363 - $is_bulk = $this->getParam('is-bulk'); 1364 - if ($is_bulk) { 1365 - $headers[] = array('Precedence', 'bulk'); 1366 - } 1367 - 1368 - if ($this->getMustEncrypt()) { 1369 - $headers[] = array('X-Phabricator-Must-Encrypt', 'Yes'); 1370 - } 1371 - 1372 - $related_phid = $this->getRelatedPHID(); 1373 - if ($related_phid) { 1374 - $headers[] = array('Thread-Topic', $related_phid); 1375 - } 1376 - 1377 - $headers[] = array('X-Phabricator-Mail-ID', $this->getID()); 1378 - 1379 - $unique = Filesystem::readRandomCharacters(16); 1380 - $headers[] = array('X-Phabricator-Send-Attempt', $unique); 1381 - 1382 - return $headers; 1383 - } 1384 - 1385 - public function getDeliveredHeaders() { 1386 - return $this->getParam('headers.sent'); 1021 + public function setDeliveredHeaders(array $headers) { 1022 + $headers = $this->flattenHeaders($headers); 1023 + return $this->setParam('headers.sent', $headers); 1387 1024 } 1388 1025 1389 1026 public function getUnfilteredHeaders() { ··· 1399 1036 return $unfiltered; 1400 1037 } 1401 1038 1039 + public function setUnfilteredHeaders(array $headers) { 1040 + $headers = $this->flattenHeaders($headers); 1041 + return $this->setParam('headers.unfiltered', $headers); 1042 + } 1043 + 1044 + private function flattenHeaders(array $headers) { 1045 + assert_instances_of($headers, 'PhabricatorMailHeader'); 1046 + 1047 + $list = array(); 1048 + foreach ($list as $header) { 1049 + $list[] = array( 1050 + $header->getName(), 1051 + $header->getValue(), 1052 + ); 1053 + } 1054 + 1055 + return $list; 1056 + } 1057 + 1402 1058 public function getDeliveredActors() { 1403 1059 return $this->getParam('actors.sent'); 1404 1060 } ··· 1415 1071 return $this->getParam('body.sent'); 1416 1072 } 1417 1073 1418 - private function filterHeaders(array $headers, $must_encrypt) { 1419 - if (!$must_encrypt) { 1420 - return $headers; 1421 - } 1422 - 1423 - $whitelist = array( 1424 - 'In-Reply-To', 1425 - 'Message-ID', 1426 - 'Precedence', 1427 - 'References', 1428 - 'Thread-Index', 1429 - 'Thread-Topic', 1430 - 1431 - 'X-Mail-Transport-Agent', 1432 - 'X-Auto-Response-Suppress', 1433 - 1434 - 'X-Phabricator-Sent-This-Message', 1435 - 'X-Phabricator-Must-Encrypt', 1436 - 'X-Phabricator-Mail-ID', 1437 - 'X-Phabricator-Send-Attempt', 1438 - ); 1439 - 1440 - // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". 1441 - // This header contains a significant amount of meaningful information 1442 - // about the object. 1443 - 1444 - $whitelist_map = array(); 1445 - foreach ($whitelist as $term) { 1446 - $whitelist_map[phutil_utf8_strtolower($term)] = true; 1447 - } 1448 - 1449 - foreach ($headers as $key => $header) { 1450 - list($name, $value) = $header; 1451 - $name = phutil_utf8_strtolower($name); 1452 - 1453 - if (!isset($whitelist_map[$name])) { 1454 - unset($headers[$key]); 1455 - } 1456 - } 1457 - 1458 - return $headers; 1074 + public function setDeliveredBody($body) { 1075 + return $this->setParam('body.sent', $body); 1459 1076 } 1460 1077 1461 1078 public function getURI() { 1462 1079 return '/mail/detail/'.$this->getID().'/'; 1463 - } 1464 - 1465 - private function newMailDomain() { 1466 - $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); 1467 - if (strlen($domain)) { 1468 - return $domain; 1469 - } 1470 - 1471 - $install_uri = PhabricatorEnv::getURI('/'); 1472 - $install_uri = new PhutilURI($install_uri); 1473 - 1474 - return $install_uri->getDomain(); 1475 - } 1476 - 1477 - public function newDefaultEmailAddress() { 1478 - $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address'); 1479 - if (strlen($raw_address)) { 1480 - return new PhutilEmailAddress($raw_address); 1481 - } 1482 - 1483 - $domain = $this->newMailDomain(); 1484 - $address = "noreply@{$domain}"; 1485 - 1486 - return new PhutilEmailAddress($address); 1487 - } 1488 - 1489 - public function newVoidEmailAddress() { 1490 - return $this->newDefaultEmailAddress(); 1491 1080 } 1492 1081 1493 1082 ··· 1576 1165 } 1577 1166 1578 1167 return PhabricatorUserPreferences::loadGlobalPreferences($viewer); 1579 - } 1580 - 1581 - private function shouldAddRePrefix(PhabricatorUserPreferences $preferences) { 1582 - $value = $preferences->getSettingValue( 1583 - PhabricatorEmailRePrefixSetting::SETTINGKEY); 1584 - 1585 - return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX); 1586 - } 1587 - 1588 - private function shouldVarySubject(PhabricatorUserPreferences $preferences) { 1589 - $value = $preferences->getSettingValue( 1590 - PhabricatorEmailVarySubjectsSetting::SETTINGKEY); 1591 - 1592 - return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS); 1593 - } 1594 - 1595 - private function shouldSendHTML(PhabricatorUserPreferences $preferences) { 1596 - $value = $preferences->getSettingValue( 1597 - PhabricatorEmailFormatSetting::SETTINGKEY); 1598 - 1599 - return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); 1600 1168 } 1601 1169 1602 1170 public function shouldRenderMailStampsInBody($viewer) {