120
final public function getRecipientsSummary(
123
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
124
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
128
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
130
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
133
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
140
final public function getRecipientsSummaryHTML(
143
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
144
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
146
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
149
$body[] = phutil_tag('strong', array(), 'To: ');
150
$body[] = phutil_implode_html(', ', mpull($to_handles, 'getName'));
151
$body[] = phutil_tag('br');
154
$body[] = phutil_tag('strong', array(), 'Cc: ');
155
$body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName'));
156
$body[] = phutil_tag('br');
158
return phutil_tag('div', array(), $body);
165
final public function multiplexMail(
166
PhabricatorMetaMTAMail $mail_template,
169
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
170
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
174
// If MetaMTA is configured to always multiplex, skip the single-email
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'));
185
if ($this->supportsPublicReplies()) {
186
$reply_to = $this->getPublicReplyHandlerEmailAddress();
187
$mail->setReplyTo($reply_to);
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.
202
$to_handles = $this->expandRecipientHandles($to_handles);
203
$cc_handles = $this->expandRecipientHandles($cc_handles);
205
$tos = mpull($to_handles, null, 'getPHID');
206
$ccs = mpull($cc_handles, null, 'getPHID');
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
211
$recipients = $tos + $ccs;
213
// When multiplexing mail, explicitly include To/Cc information in the
214
// message body and headers.
216
$mail_template = clone $mail_template;
218
$mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
219
$mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
221
$body = $mail_template->getBody();
223
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
225
$html_body = $mail_template->getHTMLBody();
226
if (strlen($html_body)) {
227
$html_body .= hsprintf('%s',
228
$this->getRecipientsSummaryHTML($to_handles, $cc_handles));
231
foreach ($recipients as $phid => $recipient) {
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));
239
// not good - they should be a to or a cc
243
$mail->setBody($body);
244
$mail->setHTMLBody($html_body);
247
if (!$reply_to && $this->supportsPrivateReplies()) {
248
$reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
251
if (!$reply_to && $this->supportsPublicReplies()) {
252
$reply_to = $this->getPublicReplyHandlerEmailAddress();
256
$mail->setReplyTo($reply_to);
265
120
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
267
122
$receiver = $this->getMailReceiver();
290
145
protected function getDefaultPrivateReplyHandlerEmailAddress(
291
PhabricatorObjectHandle $handle,
146
PhabricatorUser $user,
294
if ($handle->getType() != PhabricatorPeopleUserPHIDType::TYPECONST) {
295
// You must be a real user to get a private reply handler address.
299
$user = id(new PhabricatorPeopleQuery())
300
->setViewer(PhabricatorUser::getOmnipotentUser())
301
->withPHIDs(array($handle->getPHID()))
305
// This may happen if a user was subscribed to something, and was then
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(),
316
155
$domain = $this->getReplyHandlerDomain();
318
157
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
368
207
return rtrim($output);
371
private function expandRecipientHandles(array $handles) {
212
* Produce a list of mail targets for a given to/cc list.
214
* Each target should be sent a separate email, and contains the information
215
* required to generate it with appropriate permissions and configuration.
217
* @param list<phid> List of "To" PHIDs.
218
* @param list<phid> List of "CC" PHIDs.
219
* @return list<PhabricatorMailTarget> List of targets.
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);
376
$phids = mpull($handles, 'getPHID');
377
$results = id(new PhabricatorMetaMTAMemberQuery())
378
->setViewer(PhabricatorUser::getOmnipotentUser())
380
->executeExpansion();
382
return id(new PhabricatorHandleQuery())
383
->setViewer(PhabricatorUser::getOmnipotentUser())
384
->withPHIDs($results)
230
$template = id(new PhabricatorMailTarget())
231
->setRawToPHIDs($raw_to)
232
->setRawCCPHIDs($raw_cc);
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();
239
$template->setReplyTo($reply_to);
243
$supports_private_replies = $this->supportsPrivateReplies();
244
$mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
247
$target = id(clone $template)
248
->setViewer(PhabricatorUser::getOmnipotentUser())
252
$targets[] = $target;
256
foreach ($map as $phid => $user) {
257
$target = id(clone $template)
259
->setToMap(array($phid => $user))
262
if ($supports_private_replies) {
263
$reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
265
$target->setReplyTo($reply_to);
269
$targets[] = $target;
278
* Expand lists of recipient PHIDs.
280
* This takes any compound recipients (like projects) and looks up all their
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.
287
private function expandRecipientPHIDs(array $to, array $cc) {
288
$to_result = array();
289
$cc_result = array();
291
$all_phids = array_merge($to, $cc);
293
$map = id(new PhabricatorMetaMTAMemberQuery())
294
->setViewer(PhabricatorUser::getOmnipotentUser())
295
->withPHIDs($all_phids)
297
foreach ($to as $phid) {
298
foreach ($map[$phid] as $expanded) {
299
$to_result[$expanded] = $expanded;
302
foreach ($cc as $phid) {
303
foreach ($map[$phid] as $expanded) {
304
$cc_result[$expanded] = $expanded;
309
// Remove recipients from "CC" if they're also present in "To".
310
$cc_result = array_diff_key($cc_result, $to_result);
312
return array(array_values($to_result), array_values($cc_result));
317
* Load @{class:PhabricatorUser} objects for each recipient.
319
* Invalid recipients are dropped from the results.
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.
325
private function loadRecipientUsers(array $to, array $cc) {
326
$to_result = array();
327
$cc_result = array();
329
$all_phids = array_merge($to, $cc);
331
$users = id(new PhabricatorPeopleQuery())
332
->setViewer(PhabricatorUser::getOmnipotentUser())
333
->withPHIDs($all_phids)
335
$users = mpull($users, null, 'getPHID');
337
foreach ($to as $phid) {
338
if (isset($users[$phid])) {
339
$to_result[$phid] = $users[$phid];
342
foreach ($cc as $phid) {
343
if (isset($users[$phid])) {
344
$cc_result[$phid] = $users[$phid];
349
return array($to_result, $cc_result);
354
* Remove recipients who do not have permission to view the mail receiver.
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.
360
private function filterRecipientUsers(array $to, array $cc) {
361
$to_result = array();
362
$cc_result = array();
364
$all_users = $to + $cc;
367
$object = $this->getMailReceiver();
368
foreach ($all_users as $phid => $user) {
369
$visible = PhabricatorPolicyFilter::hasCapability(
372
PhabricatorPolicyCapability::CAN_VIEW);
374
$can_see[$phid] = true;
378
foreach ($to as $phid => $user) {
379
if (!empty($can_see[$phid])) {
380
$to_result[$phid] = $all_users[$phid];
384
foreach ($cc as $phid => $user) {
385
if (!empty($can_see[$phid])) {
386
$cc_result[$phid] = $all_users[$phid];
391
return array($to_result, $cc_result);