Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
12256 views
1
<?php
2
3
/**
4
* @task recipients Managing Recipients
5
*/
6
final class PhabricatorMetaMTAMail
7
extends PhabricatorMetaMTADAO
8
implements
9
PhabricatorPolicyInterface,
10
PhabricatorDestructibleInterface {
11
12
const RETRY_DELAY = 5;
13
14
protected $actorPHID;
15
protected $parameters = array();
16
protected $status;
17
protected $message;
18
protected $relatedPHID;
19
20
private $recipientExpansionMap;
21
private $routingMap;
22
23
public function __construct() {
24
25
$this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;
26
$this->parameters = array(
27
'sensitive' => true,
28
'mustEncrypt' => false,
29
);
30
31
parent::__construct();
32
}
33
34
protected function getConfiguration() {
35
return array(
36
self::CONFIG_AUX_PHID => true,
37
self::CONFIG_SERIALIZATION => array(
38
'parameters' => self::SERIALIZATION_JSON,
39
),
40
self::CONFIG_COLUMN_SCHEMA => array(
41
'actorPHID' => 'phid?',
42
'status' => 'text32',
43
'relatedPHID' => 'phid?',
44
45
// T6203/NULLABILITY
46
// This should just be empty if there's no body.
47
'message' => 'text?',
48
),
49
self::CONFIG_KEY_SCHEMA => array(
50
'status' => array(
51
'columns' => array('status'),
52
),
53
'key_actorPHID' => array(
54
'columns' => array('actorPHID'),
55
),
56
'relatedPHID' => array(
57
'columns' => array('relatedPHID'),
58
),
59
'key_created' => array(
60
'columns' => array('dateCreated'),
61
),
62
),
63
) + parent::getConfiguration();
64
}
65
66
public function generatePHID() {
67
return PhabricatorPHID::generateNewPHID(
68
PhabricatorMetaMTAMailPHIDType::TYPECONST);
69
}
70
71
protected function setParam($param, $value) {
72
$this->parameters[$param] = $value;
73
return $this;
74
}
75
76
protected function getParam($param, $default = null) {
77
// Some old mail was saved without parameters because no parameters were
78
// set or encoding failed. Recover in these cases so we can perform
79
// mail migrations, see T9251.
80
if (!is_array($this->parameters)) {
81
$this->parameters = array();
82
}
83
84
return idx($this->parameters, $param, $default);
85
}
86
87
/**
88
* These tags are used to allow users to opt out of receiving certain types
89
* of mail, like updates when a task's projects change.
90
*
91
* @param list<const>
92
* @return this
93
*/
94
public function setMailTags(array $tags) {
95
$this->setParam('mailtags', array_unique($tags));
96
return $this;
97
}
98
99
public function getMailTags() {
100
return $this->getParam('mailtags', array());
101
}
102
103
/**
104
* In Gmail, conversations will be broken if you reply to a thread and the
105
* server sends back a response without referencing your Message-ID, even if
106
* it references a Message-ID earlier in the thread. To avoid this, use the
107
* parent email's message ID explicitly if it's available. This overwrites the
108
* "In-Reply-To" and "References" headers we would otherwise generate. This
109
* needs to be set whenever an action is triggered by an email message. See
110
* T251 for more details.
111
*
112
* @param string The "Message-ID" of the email which precedes this one.
113
* @return this
114
*/
115
public function setParentMessageID($id) {
116
$this->setParam('parent-message-id', $id);
117
return $this;
118
}
119
120
public function getParentMessageID() {
121
return $this->getParam('parent-message-id');
122
}
123
124
public function getSubject() {
125
return $this->getParam('subject');
126
}
127
128
public function addTos(array $phids) {
129
$phids = array_unique($phids);
130
$this->setParam('to', $phids);
131
return $this;
132
}
133
134
public function addRawTos(array $raw_email) {
135
136
// Strip addresses down to bare emails, since the MailAdapter API currently
137
// requires we pass it just the address (like `[email protected]`), not
138
// a full string like `"Abraham Lincoln" <[email protected]>`.
139
foreach ($raw_email as $key => $email) {
140
$object = new PhutilEmailAddress($email);
141
$raw_email[$key] = $object->getAddress();
142
}
143
144
$this->setParam('raw-to', $raw_email);
145
return $this;
146
}
147
148
public function addCCs(array $phids) {
149
$phids = array_unique($phids);
150
$this->setParam('cc', $phids);
151
return $this;
152
}
153
154
public function setExcludeMailRecipientPHIDs(array $exclude) {
155
$this->setParam('exclude', $exclude);
156
return $this;
157
}
158
159
private function getExcludeMailRecipientPHIDs() {
160
return $this->getParam('exclude', array());
161
}
162
163
public function setMutedPHIDs(array $muted) {
164
$this->setParam('muted', $muted);
165
return $this;
166
}
167
168
private function getMutedPHIDs() {
169
return $this->getParam('muted', array());
170
}
171
172
public function setForceHeraldMailRecipientPHIDs(array $force) {
173
$this->setParam('herald-force-recipients', $force);
174
return $this;
175
}
176
177
private function getForceHeraldMailRecipientPHIDs() {
178
return $this->getParam('herald-force-recipients', array());
179
}
180
181
public function addPHIDHeaders($name, array $phids) {
182
$phids = array_unique($phids);
183
foreach ($phids as $phid) {
184
$this->addHeader($name, '<'.$phid.'>');
185
}
186
return $this;
187
}
188
189
public function addHeader($name, $value) {
190
$this->parameters['headers'][] = array($name, $value);
191
return $this;
192
}
193
194
public function getHeaders() {
195
return $this->getParam('headers', array());
196
}
197
198
public function addAttachment(PhabricatorMailAttachment $attachment) {
199
$this->parameters['attachments'][] = $attachment->toDictionary();
200
return $this;
201
}
202
203
public function getAttachments() {
204
$dicts = $this->getParam('attachments', array());
205
206
$result = array();
207
foreach ($dicts as $dict) {
208
$result[] = PhabricatorMailAttachment::newFromDictionary($dict);
209
}
210
return $result;
211
}
212
213
public function getAttachmentFilePHIDs() {
214
$file_phids = array();
215
216
$dictionaries = $this->getParam('attachments');
217
if ($dictionaries) {
218
foreach ($dictionaries as $dictionary) {
219
$file_phid = idx($dictionary, 'filePHID');
220
if ($file_phid) {
221
$file_phids[] = $file_phid;
222
}
223
}
224
}
225
226
return $file_phids;
227
}
228
229
public function loadAttachedFiles(PhabricatorUser $viewer) {
230
$file_phids = $this->getAttachmentFilePHIDs();
231
232
if (!$file_phids) {
233
return array();
234
}
235
236
return id(new PhabricatorFileQuery())
237
->setViewer($viewer)
238
->withPHIDs($file_phids)
239
->execute();
240
}
241
242
public function setAttachments(array $attachments) {
243
assert_instances_of($attachments, 'PhabricatorMailAttachment');
244
$this->setParam('attachments', mpull($attachments, 'toDictionary'));
245
return $this;
246
}
247
248
public function setFrom($from) {
249
$this->setParam('from', $from);
250
$this->setActorPHID($from);
251
return $this;
252
}
253
254
public function getFrom() {
255
return $this->getParam('from');
256
}
257
258
public function setRawFrom($raw_email, $raw_name) {
259
$this->setParam('raw-from', array($raw_email, $raw_name));
260
return $this;
261
}
262
263
public function getRawFrom() {
264
return $this->getParam('raw-from');
265
}
266
267
public function setReplyTo($reply_to) {
268
$this->setParam('reply-to', $reply_to);
269
return $this;
270
}
271
272
public function getReplyTo() {
273
return $this->getParam('reply-to');
274
}
275
276
public function setSubject($subject) {
277
$this->setParam('subject', $subject);
278
return $this;
279
}
280
281
public function setSubjectPrefix($prefix) {
282
$this->setParam('subject-prefix', $prefix);
283
return $this;
284
}
285
286
public function getSubjectPrefix() {
287
return $this->getParam('subject-prefix');
288
}
289
290
public function setVarySubjectPrefix($prefix) {
291
$this->setParam('vary-subject-prefix', $prefix);
292
return $this;
293
}
294
295
public function getVarySubjectPrefix() {
296
return $this->getParam('vary-subject-prefix');
297
}
298
299
public function setBody($body) {
300
$this->setParam('body', $body);
301
return $this;
302
}
303
304
public function setSensitiveContent($bool) {
305
$this->setParam('sensitive', $bool);
306
return $this;
307
}
308
309
public function hasSensitiveContent() {
310
return $this->getParam('sensitive', true);
311
}
312
313
public function setMustEncrypt($bool) {
314
return $this->setParam('mustEncrypt', $bool);
315
}
316
317
public function getMustEncrypt() {
318
return $this->getParam('mustEncrypt', false);
319
}
320
321
public function setMustEncryptURI($uri) {
322
return $this->setParam('mustEncrypt.uri', $uri);
323
}
324
325
public function getMustEncryptURI() {
326
return $this->getParam('mustEncrypt.uri');
327
}
328
329
public function setMustEncryptSubject($subject) {
330
return $this->setParam('mustEncrypt.subject', $subject);
331
}
332
333
public function getMustEncryptSubject() {
334
return $this->getParam('mustEncrypt.subject');
335
}
336
337
public function setMustEncryptReasons(array $reasons) {
338
return $this->setParam('mustEncryptReasons', $reasons);
339
}
340
341
public function getMustEncryptReasons() {
342
return $this->getParam('mustEncryptReasons', array());
343
}
344
345
public function setMailStamps(array $stamps) {
346
return $this->setParam('stamps', $stamps);
347
}
348
349
public function getMailStamps() {
350
return $this->getParam('stamps', array());
351
}
352
353
public function setMailStampMetadata($metadata) {
354
return $this->setParam('stampMetadata', $metadata);
355
}
356
357
public function getMailStampMetadata() {
358
return $this->getParam('stampMetadata', array());
359
}
360
361
public function getMailerKey() {
362
return $this->getParam('mailer.key');
363
}
364
365
public function setTryMailers(array $mailers) {
366
return $this->setParam('mailers.try', $mailers);
367
}
368
369
public function setHTMLBody($html) {
370
$this->setParam('html-body', $html);
371
return $this;
372
}
373
374
public function getBody() {
375
return $this->getParam('body');
376
}
377
378
public function getHTMLBody() {
379
return $this->getParam('html-body');
380
}
381
382
public function setIsErrorEmail($is_error) {
383
$this->setParam('is-error', $is_error);
384
return $this;
385
}
386
387
public function getIsErrorEmail() {
388
return $this->getParam('is-error', false);
389
}
390
391
public function getToPHIDs() {
392
return $this->getParam('to', array());
393
}
394
395
public function getRawToAddresses() {
396
return $this->getParam('raw-to', array());
397
}
398
399
public function getCcPHIDs() {
400
return $this->getParam('cc', array());
401
}
402
403
public function setMessageType($message_type) {
404
return $this->setParam('message.type', $message_type);
405
}
406
407
public function getMessageType() {
408
return $this->getParam(
409
'message.type',
410
PhabricatorMailEmailMessage::MESSAGETYPE);
411
}
412
413
414
415
/**
416
* Force delivery of a message, even if recipients have preferences which
417
* would otherwise drop the message.
418
*
419
* This is primarily intended to let users who don't want any email still
420
* receive things like password resets.
421
*
422
* @param bool True to force delivery despite user preferences.
423
* @return this
424
*/
425
public function setForceDelivery($force) {
426
$this->setParam('force', $force);
427
return $this;
428
}
429
430
public function getForceDelivery() {
431
return $this->getParam('force', false);
432
}
433
434
/**
435
* Flag that this is an auto-generated bulk message and should have bulk
436
* headers added to it if appropriate. Broadly, this means some flavor of
437
* "Precedence: bulk" or similar, but is implementation and configuration
438
* dependent.
439
*
440
* @param bool True if the mail is automated bulk mail.
441
* @return this
442
*/
443
public function setIsBulk($is_bulk) {
444
$this->setParam('is-bulk', $is_bulk);
445
return $this;
446
}
447
448
public function getIsBulk() {
449
return $this->getParam('is-bulk');
450
}
451
452
/**
453
* Use this method to set an ID used for message threading. MetaMTA will
454
* set appropriate headers (Message-ID, In-Reply-To, References and
455
* Thread-Index) based on the capabilities of the underlying mailer.
456
*
457
* @param string Unique identifier, appropriate for use in a Message-ID,
458
* In-Reply-To or References headers.
459
* @param bool If true, indicates this is the first message in the thread.
460
* @return this
461
*/
462
public function setThreadID($thread_id, $is_first_message = false) {
463
$this->setParam('thread-id', $thread_id);
464
$this->setParam('is-first-message', $is_first_message);
465
return $this;
466
}
467
468
public function getThreadID() {
469
return $this->getParam('thread-id');
470
}
471
472
public function getIsFirstMessage() {
473
return (bool)$this->getParam('is-first-message');
474
}
475
476
/**
477
* Save a newly created mail to the database. The mail will eventually be
478
* delivered by the MetaMTA daemon.
479
*
480
* @return this
481
*/
482
public function saveAndSend() {
483
return $this->save();
484
}
485
486
/**
487
* @return this
488
*/
489
public function save() {
490
if ($this->getID()) {
491
return parent::save();
492
}
493
494
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
495
// may re-enter this method from within scheduleTask(). The implementation
496
// is intended to avoid anything awkward if we end up reentering this
497
// method.
498
499
$this->openTransaction();
500
// Save to generate a mail ID and PHID.
501
$result = parent::save();
502
503
// Write the recipient edges.
504
$editor = new PhabricatorEdgeEditor();
505
$edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
506
$recipient_phids = array_merge(
507
$this->getToPHIDs(),
508
$this->getCcPHIDs());
509
$expanded_phids = $this->expandRecipients($recipient_phids);
510
$all_phids = array_unique(array_merge(
511
$recipient_phids,
512
$expanded_phids));
513
foreach ($all_phids as $curr_phid) {
514
$editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
515
}
516
$editor->save();
517
518
$this->saveTransaction();
519
520
// Queue a task to send this mail.
521
$mailer_task = PhabricatorWorker::scheduleTask(
522
'PhabricatorMetaMTAWorker',
523
$this->getID(),
524
array(
525
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
526
));
527
528
return $result;
529
}
530
531
/**
532
* Attempt to deliver an email immediately, in this process.
533
*
534
* @return void
535
*/
536
public function sendNow() {
537
if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
538
throw new Exception(pht('Trying to send an already-sent mail!'));
539
}
540
541
$mailers = self::newMailers(
542
array(
543
'outbound' => true,
544
'media' => array(
545
$this->getMessageType(),
546
),
547
));
548
549
$try_mailers = $this->getParam('mailers.try');
550
if ($try_mailers) {
551
$mailers = mpull($mailers, null, 'getKey');
552
$mailers = array_select_keys($mailers, $try_mailers);
553
}
554
555
return $this->sendWithMailers($mailers);
556
}
557
558
public static function newMailers(array $constraints) {
559
PhutilTypeSpec::checkMap(
560
$constraints,
561
array(
562
'types' => 'optional list<string>',
563
'inbound' => 'optional bool',
564
'outbound' => 'optional bool',
565
'media' => 'optional list<string>',
566
));
567
568
$mailers = array();
569
570
$config = PhabricatorEnv::getEnvConfig('cluster.mailers');
571
572
$adapters = PhabricatorMailAdapter::getAllAdapters();
573
$next_priority = -1;
574
575
foreach ($config as $spec) {
576
$type = $spec['type'];
577
if (!isset($adapters[$type])) {
578
throw new Exception(
579
pht(
580
'Unknown mailer ("%s")!',
581
$type));
582
}
583
584
$key = $spec['key'];
585
$mailer = id(clone $adapters[$type])
586
->setKey($key);
587
588
$priority = idx($spec, 'priority');
589
if (!$priority) {
590
$priority = $next_priority;
591
$next_priority--;
592
}
593
$mailer->setPriority($priority);
594
595
$defaults = $mailer->newDefaultOptions();
596
$options = idx($spec, 'options', array()) + $defaults;
597
$mailer->setOptions($options);
598
599
$mailer->setSupportsInbound(idx($spec, 'inbound', true));
600
$mailer->setSupportsOutbound(idx($spec, 'outbound', true));
601
602
$media = idx($spec, 'media');
603
if ($media !== null) {
604
$mailer->setMedia($media);
605
}
606
607
$mailers[] = $mailer;
608
}
609
610
// Remove mailers with the wrong types.
611
if (isset($constraints['types'])) {
612
$types = $constraints['types'];
613
$types = array_fuse($types);
614
foreach ($mailers as $key => $mailer) {
615
$mailer_type = $mailer->getAdapterType();
616
if (!isset($types[$mailer_type])) {
617
unset($mailers[$key]);
618
}
619
}
620
}
621
622
// If we're only looking for inbound mailers, remove mailers with inbound
623
// support disabled.
624
if (!empty($constraints['inbound'])) {
625
foreach ($mailers as $key => $mailer) {
626
if (!$mailer->getSupportsInbound()) {
627
unset($mailers[$key]);
628
}
629
}
630
}
631
632
// If we're only looking for outbound mailers, remove mailers with outbound
633
// support disabled.
634
if (!empty($constraints['outbound'])) {
635
foreach ($mailers as $key => $mailer) {
636
if (!$mailer->getSupportsOutbound()) {
637
unset($mailers[$key]);
638
}
639
}
640
}
641
642
// Select only the mailers which can transmit messages with requested media
643
// types.
644
if (!empty($constraints['media'])) {
645
foreach ($mailers as $key => $mailer) {
646
$supports_any = false;
647
foreach ($constraints['media'] as $medium) {
648
if ($mailer->supportsMessageType($medium)) {
649
$supports_any = true;
650
break;
651
}
652
}
653
654
if (!$supports_any) {
655
unset($mailers[$key]);
656
}
657
}
658
}
659
660
$sorted = array();
661
$groups = mgroup($mailers, 'getPriority');
662
krsort($groups);
663
foreach ($groups as $group) {
664
// Reorder services within the same priority group randomly.
665
shuffle($group);
666
foreach ($group as $mailer) {
667
$sorted[] = $mailer;
668
}
669
}
670
671
return $sorted;
672
}
673
674
public function sendWithMailers(array $mailers) {
675
if (!$mailers) {
676
$any_mailers = self::newMailers(array());
677
678
// NOTE: We can end up here with some custom list of "$mailers", like
679
// from a unit test. In that case, this message could be misleading. We
680
// can't really tell if the caller made up the list, so just assume they
681
// aren't tricking us.
682
683
if ($any_mailers) {
684
$void_message = pht(
685
'No configured mailers support sending outbound mail.');
686
} else {
687
$void_message = pht(
688
'No mailers are configured.');
689
}
690
691
return $this
692
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
693
->setMessage($void_message)
694
->save();
695
}
696
697
$actors = $this->loadAllActors();
698
699
// If we're sending one mail to everyone, some recipients will be in
700
// "Cc" rather than "To". We'll move them to "To" later (or supply a
701
// dummy "To") but need to look for the recipient in either the
702
// "To" or "Cc" fields here.
703
$target_phid = head($this->getToPHIDs());
704
if (!$target_phid) {
705
$target_phid = head($this->getCcPHIDs());
706
}
707
$preferences = $this->loadPreferences($target_phid);
708
709
// Attach any files we're about to send to this message, so the recipients
710
// can view them.
711
$viewer = PhabricatorUser::getOmnipotentUser();
712
$files = $this->loadAttachedFiles($viewer);
713
foreach ($files as $file) {
714
$file->attachToObject($this->getPHID());
715
}
716
717
$type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
718
$type = idx($type_map, $this->getMessageType());
719
if (!$type) {
720
throw new Exception(
721
pht(
722
'Unable to send message with unknown message type "%s".',
723
$type));
724
}
725
726
$exceptions = array();
727
foreach ($mailers as $mailer) {
728
try {
729
$message = $type->newMailMessageEngine()
730
->setMailer($mailer)
731
->setMail($this)
732
->setActors($actors)
733
->setPreferences($preferences)
734
->newMessage($mailer);
735
} catch (Exception $ex) {
736
$exceptions[] = $ex;
737
continue;
738
}
739
740
if (!$message) {
741
// If we don't get a message back, that means the mail doesn't actually
742
// need to be sent (for example, because recipients have declined to
743
// receive the mail). Void it and return.
744
return $this
745
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
746
->save();
747
}
748
749
try {
750
$mailer->sendMessage($message);
751
} catch (PhabricatorMetaMTAPermanentFailureException $ex) {
752
// If any mailer raises a permanent failure, stop trying to send the
753
// mail with other mailers.
754
$this
755
->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
756
->setMessage($ex->getMessage())
757
->save();
758
759
throw $ex;
760
} catch (Exception $ex) {
761
$exceptions[] = $ex;
762
continue;
763
}
764
765
// Keep track of which mailer actually ended up accepting the message.
766
$mailer_key = $mailer->getKey();
767
if ($mailer_key !== null) {
768
$this->setParam('mailer.key', $mailer_key);
769
}
770
771
// Now that we sent the message, store the final deliverability outcomes
772
// and reasoning so we can explain why things happened the way they did.
773
$actor_list = array();
774
foreach ($actors as $actor) {
775
$actor_list[$actor->getPHID()] = array(
776
'deliverable' => $actor->isDeliverable(),
777
'reasons' => $actor->getDeliverabilityReasons(),
778
);
779
}
780
$this->setParam('actors.sent', $actor_list);
781
$this->setParam('routing.sent', $this->getParam('routing'));
782
$this->setParam('routingmap.sent', $this->getRoutingRuleMap());
783
784
return $this
785
->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)
786
->save();
787
}
788
789
// If we make it here, no mailer could send the mail but no mailer failed
790
// permanently either. We update the error message for the mail, but leave
791
// it in the current status (usually, STATUS_QUEUE) and try again later.
792
793
$messages = array();
794
foreach ($exceptions as $ex) {
795
$messages[] = $ex->getMessage();
796
}
797
$messages = implode("\n\n", $messages);
798
799
$this
800
->setMessage($messages)
801
->save();
802
803
if (count($exceptions) === 1) {
804
throw head($exceptions);
805
}
806
807
throw new PhutilAggregateException(
808
pht('Encountered multiple exceptions while transmitting mail.'),
809
$exceptions);
810
}
811
812
813
public static function shouldMailEachRecipient() {
814
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
815
}
816
817
818
/* -( Managing Recipients )------------------------------------------------ */
819
820
821
/**
822
* Get all of the recipients for this mail, after preference filters are
823
* applied. This list has all objects to whom delivery will be attempted.
824
*
825
* Note that this expands recipients into their members, because delivery
826
* is never directly attempted to aggregate actors like projects.
827
*
828
* @return list<phid> A list of all recipients to whom delivery will be
829
* attempted.
830
* @task recipients
831
*/
832
public function buildRecipientList() {
833
$actors = $this->loadAllActors();
834
$actors = $this->filterDeliverableActors($actors);
835
return mpull($actors, 'getPHID');
836
}
837
838
public function loadAllActors() {
839
$actor_phids = $this->getExpandedRecipientPHIDs();
840
return $this->loadActors($actor_phids);
841
}
842
843
public function getExpandedRecipientPHIDs() {
844
$actor_phids = $this->getAllActorPHIDs();
845
return $this->expandRecipients($actor_phids);
846
}
847
848
private function getAllActorPHIDs() {
849
return array_merge(
850
array($this->getParam('from')),
851
$this->getToPHIDs(),
852
$this->getCcPHIDs());
853
}
854
855
/**
856
* Expand a list of recipient PHIDs (possibly including aggregate recipients
857
* like projects) into a deaggregated list of individual recipient PHIDs.
858
* For example, this will expand project PHIDs into a list of the project's
859
* members.
860
*
861
* @param list<phid> List of recipient PHIDs, possibly including aggregate
862
* recipients.
863
* @return list<phid> Deaggregated list of mailable recipients.
864
*/
865
public function expandRecipients(array $phids) {
866
if ($this->recipientExpansionMap === null) {
867
$all_phids = $this->getAllActorPHIDs();
868
$this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
869
->setViewer(PhabricatorUser::getOmnipotentUser())
870
->withPHIDs($all_phids)
871
->execute();
872
}
873
874
$results = array();
875
foreach ($phids as $phid) {
876
foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
877
$results[$recipient_phid] = $recipient_phid;
878
}
879
}
880
881
return array_keys($results);
882
}
883
884
private function filterDeliverableActors(array $actors) {
885
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
886
$deliverable_actors = array();
887
foreach ($actors as $phid => $actor) {
888
if ($actor->isDeliverable()) {
889
$deliverable_actors[$phid] = $actor;
890
}
891
}
892
return $deliverable_actors;
893
}
894
895
private function loadActors(array $actor_phids) {
896
$actor_phids = array_filter($actor_phids);
897
$viewer = PhabricatorUser::getOmnipotentUser();
898
899
$actors = id(new PhabricatorMetaMTAActorQuery())
900
->setViewer($viewer)
901
->withPHIDs($actor_phids)
902
->execute();
903
904
if (!$actors) {
905
return array();
906
}
907
908
if ($this->getForceDelivery()) {
909
// If we're forcing delivery, skip all the opt-out checks. We don't
910
// bother annotating reasoning on the mail in this case because it should
911
// always be obvious why the mail hit this rule (e.g., it is a password
912
// reset mail).
913
foreach ($actors as $actor) {
914
$actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
915
}
916
return $actors;
917
}
918
919
// Exclude explicit recipients.
920
foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
921
$actor = idx($actors, $phid);
922
if (!$actor) {
923
continue;
924
}
925
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
926
}
927
928
// Before running more rules, save a list of the actors who were
929
// deliverable before we started running preference-based rules. This stops
930
// us from trying to send mail to disabled users just because a Herald rule
931
// added them, for example.
932
$deliverable = array();
933
foreach ($actors as $phid => $actor) {
934
if ($actor->isDeliverable()) {
935
$deliverable[] = $phid;
936
}
937
}
938
939
// Exclude muted recipients. We're doing this after saving deliverability
940
// so that Herald "Send me an email" actions can still punch through a
941
// mute.
942
943
foreach ($this->getMutedPHIDs() as $muted_phid) {
944
$muted_actor = idx($actors, $muted_phid);
945
if (!$muted_actor) {
946
continue;
947
}
948
$muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
949
}
950
951
// For the rest of the rules, order matters. We're going to run all the
952
// possible rules in order from weakest to strongest, and let the strongest
953
// matching rule win. The weaker rules leave annotations behind which help
954
// users understand why the mail was routed the way it was.
955
956
// Exclude the actor if their preferences are set.
957
$from_phid = $this->getParam('from');
958
$from_actor = idx($actors, $from_phid);
959
if ($from_actor) {
960
$from_user = id(new PhabricatorPeopleQuery())
961
->setViewer($viewer)
962
->withPHIDs(array($from_phid))
963
->needUserSettings(true)
964
->execute();
965
$from_user = head($from_user);
966
if ($from_user) {
967
$pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;
968
$exclude_self = $from_user->getUserSetting($pref_key);
969
if ($exclude_self) {
970
$from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
971
}
972
}
973
}
974
975
$all_prefs = id(new PhabricatorUserPreferencesQuery())
976
->setViewer(PhabricatorUser::getOmnipotentUser())
977
->withUserPHIDs($actor_phids)
978
->needSyntheticPreferences(true)
979
->execute();
980
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
981
982
$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
983
984
// Exclude all recipients who have set preferences to not receive this type
985
// of email (for example, a user who says they don't want emails about task
986
// CC changes).
987
$tags = $this->getParam('mailtags');
988
if ($tags) {
989
foreach ($all_prefs as $phid => $prefs) {
990
$user_mailtags = $prefs->getSettingValue(
991
PhabricatorEmailTagsSetting::SETTINGKEY);
992
993
// The user must have elected to receive mail for at least one
994
// of the mailtags.
995
$send = false;
996
foreach ($tags as $tag) {
997
if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
998
$send = true;
999
break;
1000
}
1001
}
1002
1003
if (!$send) {
1004
$actors[$phid]->setUndeliverable(
1005
PhabricatorMetaMTAActor::REASON_MAILTAGS);
1006
}
1007
}
1008
}
1009
1010
foreach ($deliverable as $phid) {
1011
switch ($this->getRoutingRule($phid)) {
1012
case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
1013
$actors[$phid]->setUndeliverable(
1014
PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);
1015
break;
1016
case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
1017
$actors[$phid]->setDeliverable(
1018
PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);
1019
break;
1020
default:
1021
// No change.
1022
break;
1023
}
1024
}
1025
1026
// If recipients were initially deliverable and were added by "Send me an
1027
// email" Herald rules, annotate them as such and make them deliverable
1028
// again, overriding any changes made by the "self mail" and "mail tags"
1029
// settings.
1030
$force_recipients = $this->getForceHeraldMailRecipientPHIDs();
1031
$force_recipients = array_fuse($force_recipients);
1032
if ($force_recipients) {
1033
foreach ($deliverable as $phid) {
1034
if (isset($force_recipients[$phid])) {
1035
$actors[$phid]->setDeliverable(
1036
PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
1037
}
1038
}
1039
}
1040
1041
// Exclude recipients who don't want any mail. This rule is very strong
1042
// and runs last.
1043
foreach ($all_prefs as $phid => $prefs) {
1044
$exclude = $prefs->getSettingValue(
1045
PhabricatorEmailNotificationsSetting::SETTINGKEY);
1046
if ($exclude) {
1047
$actors[$phid]->setUndeliverable(
1048
PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
1049
}
1050
}
1051
1052
// Unless delivery was forced earlier (password resets, confirmation mail),
1053
// never send mail to unverified addresses.
1054
foreach ($actors as $phid => $actor) {
1055
if ($actor->getIsVerified()) {
1056
continue;
1057
}
1058
1059
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);
1060
}
1061
1062
return $actors;
1063
}
1064
1065
public function getDeliveredHeaders() {
1066
return $this->getParam('headers.sent');
1067
}
1068
1069
public function setDeliveredHeaders(array $headers) {
1070
$headers = $this->flattenHeaders($headers);
1071
return $this->setParam('headers.sent', $headers);
1072
}
1073
1074
public function getUnfilteredHeaders() {
1075
$unfiltered = $this->getParam('headers.unfiltered');
1076
1077
if ($unfiltered === null) {
1078
// Older versions of Phabricator did not filter headers, and thus did
1079
// not record unfiltered headers. If we don't have unfiltered header
1080
// data just return the delivered headers for compatibility.
1081
return $this->getDeliveredHeaders();
1082
}
1083
1084
return $unfiltered;
1085
}
1086
1087
public function setUnfilteredHeaders(array $headers) {
1088
$headers = $this->flattenHeaders($headers);
1089
return $this->setParam('headers.unfiltered', $headers);
1090
}
1091
1092
private function flattenHeaders(array $headers) {
1093
assert_instances_of($headers, 'PhabricatorMailHeader');
1094
1095
$list = array();
1096
foreach ($list as $header) {
1097
$list[] = array(
1098
$header->getName(),
1099
$header->getValue(),
1100
);
1101
}
1102
1103
return $list;
1104
}
1105
1106
public function getDeliveredActors() {
1107
return $this->getParam('actors.sent');
1108
}
1109
1110
public function getDeliveredRoutingRules() {
1111
return $this->getParam('routing.sent');
1112
}
1113
1114
public function getDeliveredRoutingMap() {
1115
return $this->getParam('routingmap.sent');
1116
}
1117
1118
public function getDeliveredBody() {
1119
return $this->getParam('body.sent');
1120
}
1121
1122
public function setDeliveredBody($body) {
1123
return $this->setParam('body.sent', $body);
1124
}
1125
1126
public function getURI() {
1127
return '/mail/detail/'.$this->getID().'/';
1128
}
1129
1130
1131
/* -( Routing )------------------------------------------------------------ */
1132
1133
1134
public function addRoutingRule($routing_rule, $phids, $reason_phid) {
1135
$routing = $this->getParam('routing', array());
1136
$routing[] = array(
1137
'routingRule' => $routing_rule,
1138
'phids' => $phids,
1139
'reasonPHID' => $reason_phid,
1140
);
1141
$this->setParam('routing', $routing);
1142
1143
// Throw the routing map away so we rebuild it.
1144
$this->routingMap = null;
1145
1146
return $this;
1147
}
1148
1149
private function getRoutingRule($phid) {
1150
$map = $this->getRoutingRuleMap();
1151
1152
$info = idx($map, $phid, idx($map, 'default'));
1153
if ($info) {
1154
return idx($info, 'rule');
1155
}
1156
1157
return null;
1158
}
1159
1160
private function getRoutingRuleMap() {
1161
if ($this->routingMap === null) {
1162
$map = array();
1163
1164
$routing = $this->getParam('routing', array());
1165
foreach ($routing as $route) {
1166
$phids = $route['phids'];
1167
if ($phids === null) {
1168
$phids = array('default');
1169
}
1170
1171
foreach ($phids as $phid) {
1172
$new_rule = $route['routingRule'];
1173
1174
$current_rule = idx($map, $phid);
1175
if ($current_rule === null) {
1176
$is_stronger = true;
1177
} else {
1178
$is_stronger = PhabricatorMailRoutingRule::isStrongerThan(
1179
$new_rule,
1180
$current_rule);
1181
}
1182
1183
if ($is_stronger) {
1184
$map[$phid] = array(
1185
'rule' => $new_rule,
1186
'reason' => $route['reasonPHID'],
1187
);
1188
}
1189
}
1190
}
1191
1192
$this->routingMap = $map;
1193
}
1194
1195
return $this->routingMap;
1196
}
1197
1198
/* -( Preferences )-------------------------------------------------------- */
1199
1200
1201
private function loadPreferences($target_phid) {
1202
$viewer = PhabricatorUser::getOmnipotentUser();
1203
1204
if (self::shouldMailEachRecipient()) {
1205
$preferences = id(new PhabricatorUserPreferencesQuery())
1206
->setViewer($viewer)
1207
->withUserPHIDs(array($target_phid))
1208
->needSyntheticPreferences(true)
1209
->executeOne();
1210
if ($preferences) {
1211
return $preferences;
1212
}
1213
}
1214
1215
return PhabricatorUserPreferences::loadGlobalPreferences($viewer);
1216
}
1217
1218
public function shouldRenderMailStampsInBody($viewer) {
1219
$preferences = $this->loadPreferences($viewer->getPHID());
1220
$value = $preferences->getSettingValue(
1221
PhabricatorEmailStampsSetting::SETTINGKEY);
1222
1223
return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);
1224
}
1225
1226
1227
/* -( PhabricatorPolicyInterface )----------------------------------------- */
1228
1229
1230
public function getCapabilities() {
1231
return array(
1232
PhabricatorPolicyCapability::CAN_VIEW,
1233
);
1234
}
1235
1236
public function getPolicy($capability) {
1237
return PhabricatorPolicies::POLICY_NOONE;
1238
}
1239
1240
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1241
$actor_phids = $this->getExpandedRecipientPHIDs();
1242
return in_array($viewer->getPHID(), $actor_phids);
1243
}
1244
1245
public function describeAutomaticCapability($capability) {
1246
return pht(
1247
'The mail sender and message recipients can always see the mail.');
1248
}
1249
1250
1251
/* -( PhabricatorDestructibleInterface )----------------------------------- */
1252
1253
1254
public function destroyObjectPermanently(
1255
PhabricatorDestructionEngine $engine) {
1256
1257
$files = $this->loadAttachedFiles($engine->getViewer());
1258
foreach ($files as $file) {
1259
$engine->destroyObject($file);
1260
}
1261
1262
$this->delete();
1263
}
1264
1265
}
1266
1267