Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F548805
PhabricatorMetaMTAMail.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
35 KB
Referenced Files
None
Subscribers
None
PhabricatorMetaMTAMail.php
View Options
<?php
/**
* @task recipients Managing Recipients
*/
final
class
PhabricatorMetaMTAMail
extends
PhabricatorMetaMTADAO
implements
PhabricatorPolicyInterface
,
PhabricatorDestructibleInterface
{
const
RETRY_DELAY
=
5
;
protected
$actorPHID
;
protected
$parameters
=
array
();
protected
$status
;
protected
$message
;
protected
$relatedPHID
;
private
$recipientExpansionMap
;
private
$routingMap
;
public
function
__construct
()
{
$this
->
status
=
PhabricatorMailOutboundStatus
::
STATUS_QUEUE
;
$this
->
parameters
=
array
(
'sensitive'
=>
true
,
'mustEncrypt'
=>
false
,
);
parent
::
__construct
();
}
protected
function
getConfiguration
()
{
return
array
(
self
::
CONFIG_AUX_PHID
=>
true
,
self
::
CONFIG_SERIALIZATION
=>
array
(
'parameters'
=>
self
::
SERIALIZATION_JSON
,
),
self
::
CONFIG_COLUMN_SCHEMA
=>
array
(
'actorPHID'
=>
'phid?'
,
'status'
=>
'text32'
,
'relatedPHID'
=>
'phid?'
,
// T6203/NULLABILITY
// This should just be empty if there's no body.
'message'
=>
'text?'
,
),
self
::
CONFIG_KEY_SCHEMA
=>
array
(
'status'
=>
array
(
'columns'
=>
array
(
'status'
),
),
'key_actorPHID'
=>
array
(
'columns'
=>
array
(
'actorPHID'
),
),
'relatedPHID'
=>
array
(
'columns'
=>
array
(
'relatedPHID'
),
),
'key_created'
=>
array
(
'columns'
=>
array
(
'dateCreated'
),
),
),
)
+
parent
::
getConfiguration
();
}
public
function
generatePHID
()
{
return
PhabricatorPHID
::
generateNewPHID
(
PhabricatorMetaMTAMailPHIDType
::
TYPECONST
);
}
protected
function
setParam
(
$param
,
$value
)
{
$this
->
parameters
[
$param
]
=
$value
;
return
$this
;
}
protected
function
getParam
(
$param
,
$default
=
null
)
{
// Some old mail was saved without parameters because no parameters were
// set or encoding failed. Recover in these cases so we can perform
// mail migrations, see T9251.
if
(!
is_array
(
$this
->
parameters
))
{
$this
->
parameters
=
array
();
}
return
idx
(
$this
->
parameters
,
$param
,
$default
);
}
/**
* These tags are used to allow users to opt out of receiving certain types
* of mail, like updates when a task's projects change.
*
* @param list<const> $tags
* @return $this
*/
public
function
setMailTags
(
array
$tags
)
{
$this
->
setParam
(
'mailtags'
,
array_unique
(
$tags
));
return
$this
;
}
public
function
getMailTags
()
{
return
$this
->
getParam
(
'mailtags'
,
array
());
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string $id The "Message-ID" of the email which precedes this one.
* @return $this
*/
public
function
setParentMessageID
(
$id
)
{
$this
->
setParam
(
'parent-message-id'
,
$id
);
return
$this
;
}
public
function
getParentMessageID
()
{
return
$this
->
getParam
(
'parent-message-id'
);
}
public
function
getSubject
()
{
return
$this
->
getParam
(
'subject'
);
}
public
function
addTos
(
array
$phids
)
{
$phids
=
array_unique
(
$phids
);
$this
->
setParam
(
'to'
,
$phids
);
return
$this
;
}
public
function
addRawTos
(
array
$raw_email
)
{
// Strip addresses down to bare emails, since the MailAdapter API currently
// requires we pass it just the address (like `alincoln@logcabin.org`), not
// a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
foreach
(
$raw_email
as
$key
=>
$email
)
{
$object
=
new
PhutilEmailAddress
(
$email
);
$raw_email
[
$key
]
=
$object
->
getAddress
();
}
$this
->
setParam
(
'raw-to'
,
$raw_email
);
return
$this
;
}
public
function
addCCs
(
array
$phids
)
{
$phids
=
array_unique
(
$phids
);
$this
->
setParam
(
'cc'
,
$phids
);
return
$this
;
}
public
function
setExcludeMailRecipientPHIDs
(
array
$exclude
)
{
$this
->
setParam
(
'exclude'
,
$exclude
);
return
$this
;
}
private
function
getExcludeMailRecipientPHIDs
()
{
return
$this
->
getParam
(
'exclude'
,
array
());
}
public
function
setMutedPHIDs
(
array
$muted
)
{
$this
->
setParam
(
'muted'
,
$muted
);
return
$this
;
}
private
function
getMutedPHIDs
()
{
return
$this
->
getParam
(
'muted'
,
array
());
}
public
function
setForceHeraldMailRecipientPHIDs
(
array
$force
)
{
$this
->
setParam
(
'herald-force-recipients'
,
$force
);
return
$this
;
}
private
function
getForceHeraldMailRecipientPHIDs
()
{
return
$this
->
getParam
(
'herald-force-recipients'
,
array
());
}
public
function
addPHIDHeaders
(
$name
,
array
$phids
)
{
$phids
=
array_unique
(
$phids
);
foreach
(
$phids
as
$phid
)
{
$this
->
addHeader
(
$name
,
'<'
.
$phid
.
'>'
);
}
return
$this
;
}
public
function
addHeader
(
$name
,
$value
)
{
$this
->
parameters
[
'headers'
][]
=
array
(
$name
,
$value
);
return
$this
;
}
public
function
getHeaders
()
{
return
$this
->
getParam
(
'headers'
,
array
());
}
public
function
addAttachment
(
PhabricatorMailAttachment
$attachment
)
{
$this
->
parameters
[
'attachments'
][]
=
$attachment
->
toDictionary
();
return
$this
;
}
public
function
getAttachments
()
{
$dicts
=
$this
->
getParam
(
'attachments'
,
array
());
$result
=
array
();
foreach
(
$dicts
as
$dict
)
{
$result
[]
=
PhabricatorMailAttachment
::
newFromDictionary
(
$dict
);
}
return
$result
;
}
public
function
getAttachmentFilePHIDs
()
{
$file_phids
=
array
();
$dictionaries
=
$this
->
getParam
(
'attachments'
);
if
(
$dictionaries
)
{
foreach
(
$dictionaries
as
$dictionary
)
{
$file_phid
=
idx
(
$dictionary
,
'filePHID'
);
if
(
$file_phid
)
{
$file_phids
[]
=
$file_phid
;
}
}
}
return
$file_phids
;
}
public
function
loadAttachedFiles
(
PhabricatorUser
$viewer
)
{
$file_phids
=
$this
->
getAttachmentFilePHIDs
();
if
(!
$file_phids
)
{
return
array
();
}
return
id
(
new
PhabricatorFileQuery
())
->
setViewer
(
$viewer
)
->
withPHIDs
(
$file_phids
)
->
execute
();
}
public
function
setAttachments
(
array
$attachments
)
{
assert_instances_of
(
$attachments
,
'PhabricatorMailAttachment'
);
$this
->
setParam
(
'attachments'
,
mpull
(
$attachments
,
'toDictionary'
));
return
$this
;
}
public
function
setFrom
(
$from
)
{
$this
->
setParam
(
'from'
,
$from
);
$this
->
setActorPHID
(
$from
);
return
$this
;
}
public
function
getFrom
()
{
return
$this
->
getParam
(
'from'
);
}
public
function
setRawFrom
(
$raw_email
,
$raw_name
)
{
$this
->
setParam
(
'raw-from'
,
array
(
$raw_email
,
$raw_name
));
return
$this
;
}
public
function
getRawFrom
()
{
return
$this
->
getParam
(
'raw-from'
);
}
public
function
setReplyTo
(
$reply_to
)
{
$this
->
setParam
(
'reply-to'
,
$reply_to
);
return
$this
;
}
public
function
getReplyTo
()
{
return
$this
->
getParam
(
'reply-to'
);
}
public
function
setSubject
(
$subject
)
{
$this
->
setParam
(
'subject'
,
$subject
);
return
$this
;
}
public
function
setSubjectPrefix
(
$prefix
)
{
$this
->
setParam
(
'subject-prefix'
,
$prefix
);
return
$this
;
}
public
function
getSubjectPrefix
()
{
return
$this
->
getParam
(
'subject-prefix'
);
}
public
function
setVarySubjectPrefix
(
$prefix
)
{
$this
->
setParam
(
'vary-subject-prefix'
,
$prefix
);
return
$this
;
}
public
function
getVarySubjectPrefix
()
{
return
$this
->
getParam
(
'vary-subject-prefix'
);
}
public
function
setBody
(
$body
)
{
$this
->
setParam
(
'body'
,
$body
);
return
$this
;
}
public
function
setSensitiveContent
(
$bool
)
{
$this
->
setParam
(
'sensitive'
,
$bool
);
return
$this
;
}
public
function
hasSensitiveContent
()
{
return
$this
->
getParam
(
'sensitive'
,
true
);
}
public
function
setMustEncrypt
(
$bool
)
{
return
$this
->
setParam
(
'mustEncrypt'
,
$bool
);
}
public
function
getMustEncrypt
()
{
return
$this
->
getParam
(
'mustEncrypt'
,
false
);
}
public
function
setMustEncryptURI
(
$uri
)
{
return
$this
->
setParam
(
'mustEncrypt.uri'
,
$uri
);
}
public
function
getMustEncryptURI
()
{
return
$this
->
getParam
(
'mustEncrypt.uri'
);
}
public
function
setMustEncryptSubject
(
$subject
)
{
return
$this
->
setParam
(
'mustEncrypt.subject'
,
$subject
);
}
public
function
getMustEncryptSubject
()
{
return
$this
->
getParam
(
'mustEncrypt.subject'
);
}
public
function
setMustEncryptReasons
(
array
$reasons
)
{
return
$this
->
setParam
(
'mustEncryptReasons'
,
$reasons
);
}
public
function
getMustEncryptReasons
()
{
return
$this
->
getParam
(
'mustEncryptReasons'
,
array
());
}
public
function
setMailStamps
(
array
$stamps
)
{
return
$this
->
setParam
(
'stamps'
,
$stamps
);
}
public
function
getMailStamps
()
{
return
$this
->
getParam
(
'stamps'
,
array
());
}
public
function
setMailStampMetadata
(
$metadata
)
{
return
$this
->
setParam
(
'stampMetadata'
,
$metadata
);
}
public
function
getMailStampMetadata
()
{
return
$this
->
getParam
(
'stampMetadata'
,
array
());
}
public
function
getMailerKey
()
{
return
$this
->
getParam
(
'mailer.key'
);
}
public
function
setTryMailers
(
array
$mailers
)
{
return
$this
->
setParam
(
'mailers.try'
,
$mailers
);
}
public
function
setHTMLBody
(
$html
)
{
$this
->
setParam
(
'html-body'
,
$html
);
return
$this
;
}
public
function
getBody
()
{
return
$this
->
getParam
(
'body'
);
}
public
function
getHTMLBody
()
{
return
$this
->
getParam
(
'html-body'
);
}
public
function
setIsErrorEmail
(
$is_error
)
{
$this
->
setParam
(
'is-error'
,
$is_error
);
return
$this
;
}
public
function
getIsErrorEmail
()
{
return
$this
->
getParam
(
'is-error'
,
false
);
}
public
function
getToPHIDs
()
{
return
$this
->
getParam
(
'to'
,
array
());
}
public
function
getRawToAddresses
()
{
return
$this
->
getParam
(
'raw-to'
,
array
());
}
public
function
getCcPHIDs
()
{
return
$this
->
getParam
(
'cc'
,
array
());
}
public
function
setMessageType
(
$message_type
)
{
return
$this
->
setParam
(
'message.type'
,
$message_type
);
}
public
function
getMessageType
()
{
return
$this
->
getParam
(
'message.type'
,
PhabricatorMailEmailMessage
::
MESSAGETYPE
);
}
/**
* Force delivery of a message, even if recipients have preferences which
* would otherwise drop the message.
*
* This is primarily intended to let users who don't want any email still
* receive things like password resets.
*
* @param bool $force True to force delivery despite user preferences.
* @return $this
*/
public
function
setForceDelivery
(
$force
)
{
$this
->
setParam
(
'force'
,
$force
);
return
$this
;
}
public
function
getForceDelivery
()
{
return
$this
->
getParam
(
'force'
,
false
);
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool $is_bulk True if the mail is automated bulk mail.
* @return $this
*/
public
function
setIsBulk
(
$is_bulk
)
{
$this
->
setParam
(
'is-bulk'
,
$is_bulk
);
return
$this
;
}
public
function
getIsBulk
()
{
return
$this
->
getParam
(
'is-bulk'
);
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string $thread_id Unique identifier, appropriate for use in a
* Message-ID, In-Reply-To or References headers.
* @param bool $is_first_message (optional) If true, indicates this is the
* first message in the thread.
* @return $this
*/
public
function
setThreadID
(
$thread_id
,
$is_first_message
=
false
)
{
$this
->
setParam
(
'thread-id'
,
$thread_id
);
$this
->
setParam
(
'is-first-message'
,
$is_first_message
);
return
$this
;
}
public
function
getThreadID
()
{
return
$this
->
getParam
(
'thread-id'
);
}
public
function
getIsFirstMessage
()
{
return
(
bool
)
$this
->
getParam
(
'is-first-message'
);
}
/**
* Save a newly created mail to the database. The mail will eventually be
* delivered by the MetaMTA daemon.
*
* @return $this
*/
public
function
saveAndSend
()
{
return
$this
->
save
();
}
/**
* @return $this
*/
public
function
save
()
{
if
(
$this
->
getID
())
{
return
parent
::
save
();
}
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
// may re-enter this method from within scheduleTask(). The implementation
// is intended to avoid anything awkward if we end up reentering this
// method.
$this
->
openTransaction
();
// Save to generate a mail ID and PHID.
$result
=
parent
::
save
();
// Write the recipient edges.
$editor
=
new
PhabricatorEdgeEditor
();
$edge_type
=
PhabricatorMetaMTAMailHasRecipientEdgeType
::
EDGECONST
;
$recipient_phids
=
array_merge
(
$this
->
getToPHIDs
(),
$this
->
getCcPHIDs
());
$expanded_phids
=
$this
->
expandRecipients
(
$recipient_phids
);
$all_phids
=
array_unique
(
array_merge
(
$recipient_phids
,
$expanded_phids
));
foreach
(
$all_phids
as
$curr_phid
)
{
$editor
->
addEdge
(
$this
->
getPHID
(),
$edge_type
,
$curr_phid
);
}
$editor
->
save
();
$this
->
saveTransaction
();
// Queue a task to send this mail.
$mailer_task
=
PhabricatorWorker
::
scheduleTask
(
'PhabricatorMetaMTAWorker'
,
$this
->
getID
(),
array
(
'priority'
=>
PhabricatorWorker
::
PRIORITY_ALERTS
,
));
return
$result
;
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @return void
*/
public
function
sendNow
()
{
if
(
$this
->
getStatus
()
!=
PhabricatorMailOutboundStatus
::
STATUS_QUEUE
)
{
throw
new
Exception
(
pht
(
'Trying to send an already-sent mail!'
));
}
$mailers
=
self
::
newMailers
(
array
(
'outbound'
=>
true
,
'media'
=>
array
(
$this
->
getMessageType
(),
),
));
$try_mailers
=
$this
->
getParam
(
'mailers.try'
);
if
(
$try_mailers
)
{
$mailers
=
mpull
(
$mailers
,
null
,
'getKey'
);
$mailers
=
array_select_keys
(
$mailers
,
$try_mailers
);
}
return
$this
->
sendWithMailers
(
$mailers
);
}
public
static
function
newMailers
(
array
$constraints
)
{
PhutilTypeSpec
::
checkMap
(
$constraints
,
array
(
'types'
=>
'optional list<string>'
,
'inbound'
=>
'optional bool'
,
'outbound'
=>
'optional bool'
,
'media'
=>
'optional list<string>'
,
));
$mailers
=
array
();
$config
=
PhabricatorEnv
::
getEnvConfig
(
'cluster.mailers'
);
$adapters
=
PhabricatorMailAdapter
::
getAllAdapters
();
$next_priority
=
-
1
;
foreach
(
$config
as
$spec
)
{
$type
=
$spec
[
'type'
];
if
(!
isset
(
$adapters
[
$type
]))
{
throw
new
Exception
(
pht
(
'Unknown mailer ("%s")!'
,
$type
));
}
$key
=
$spec
[
'key'
];
$mailer
=
id
(
clone
$adapters
[
$type
])
->
setKey
(
$key
);
$priority
=
idx
(
$spec
,
'priority'
);
if
(!
$priority
)
{
$priority
=
$next_priority
;
$next_priority
--;
}
$mailer
->
setPriority
(
$priority
);
$defaults
=
$mailer
->
newDefaultOptions
();
$options
=
idx
(
$spec
,
'options'
,
array
())
+
$defaults
;
$mailer
->
setOptions
(
$options
);
$mailer
->
setSupportsInbound
(
idx
(
$spec
,
'inbound'
,
true
));
$mailer
->
setSupportsOutbound
(
idx
(
$spec
,
'outbound'
,
true
));
$media
=
idx
(
$spec
,
'media'
);
if
(
$media
!==
null
)
{
$mailer
->
setMedia
(
$media
);
}
$mailers
[]
=
$mailer
;
}
// Remove mailers with the wrong types.
if
(
isset
(
$constraints
[
'types'
]))
{
$types
=
$constraints
[
'types'
];
$types
=
array_fuse
(
$types
);
foreach
(
$mailers
as
$key
=>
$mailer
)
{
$mailer_type
=
$mailer
->
getAdapterType
();
if
(!
isset
(
$types
[
$mailer_type
]))
{
unset
(
$mailers
[
$key
]);
}
}
}
// If we're only looking for inbound mailers, remove mailers with inbound
// support disabled.
if
(!
empty
(
$constraints
[
'inbound'
]))
{
foreach
(
$mailers
as
$key
=>
$mailer
)
{
if
(!
$mailer
->
getSupportsInbound
())
{
unset
(
$mailers
[
$key
]);
}
}
}
// If we're only looking for outbound mailers, remove mailers with outbound
// support disabled.
if
(!
empty
(
$constraints
[
'outbound'
]))
{
foreach
(
$mailers
as
$key
=>
$mailer
)
{
if
(!
$mailer
->
getSupportsOutbound
())
{
unset
(
$mailers
[
$key
]);
}
}
}
// Select only the mailers which can transmit messages with requested media
// types.
if
(!
empty
(
$constraints
[
'media'
]))
{
foreach
(
$mailers
as
$key
=>
$mailer
)
{
$supports_any
=
false
;
foreach
(
$constraints
[
'media'
]
as
$medium
)
{
if
(
$mailer
->
supportsMessageType
(
$medium
))
{
$supports_any
=
true
;
break
;
}
}
if
(!
$supports_any
)
{
unset
(
$mailers
[
$key
]);
}
}
}
$sorted
=
array
();
$groups
=
mgroup
(
$mailers
,
'getPriority'
);
krsort
(
$groups
);
foreach
(
$groups
as
$group
)
{
// Reorder services within the same priority group randomly.
shuffle
(
$group
);
foreach
(
$group
as
$mailer
)
{
$sorted
[]
=
$mailer
;
}
}
return
$sorted
;
}
public
function
sendWithMailers
(
array
$mailers
)
{
if
(!
$mailers
)
{
$any_mailers
=
self
::
newMailers
(
array
());
// NOTE: We can end up here with some custom list of "$mailers", like
// from a unit test. In that case, this message could be misleading. We
// can't really tell if the caller made up the list, so just assume they
// aren't tricking us.
if
(
$any_mailers
)
{
$void_message
=
pht
(
'No configured mailers support sending outbound mail.'
);
}
else
{
$void_message
=
pht
(
'No mailers are configured.'
);
}
return
$this
->
setStatus
(
PhabricatorMailOutboundStatus
::
STATUS_VOID
)
->
setMessage
(
$void_message
)
->
save
();
}
$actors
=
$this
->
loadAllActors
();
// If we're sending one mail to everyone, some recipients will be in
// "Cc" rather than "To". We'll move them to "To" later (or supply a
// dummy "To") but need to look for the recipient in either the
// "To" or "Cc" fields here.
$target_phid
=
head
(
$this
->
getToPHIDs
());
if
(!
$target_phid
)
{
$target_phid
=
head
(
$this
->
getCcPHIDs
());
}
$preferences
=
$this
->
loadPreferences
(
$target_phid
);
// Attach any files we're about to send to this message, so the recipients
// can view them.
$viewer
=
PhabricatorUser
::
getOmnipotentUser
();
$files
=
$this
->
loadAttachedFiles
(
$viewer
);
foreach
(
$files
as
$file
)
{
$file
->
attachToObject
(
$this
->
getPHID
());
}
$type_map
=
PhabricatorMailExternalMessage
::
getAllMessageTypes
();
$type
=
idx
(
$type_map
,
$this
->
getMessageType
());
if
(!
$type
)
{
throw
new
Exception
(
pht
(
'Unable to send message with unknown message type "%s".'
,
$type
));
}
$exceptions
=
array
();
foreach
(
$mailers
as
$mailer
)
{
try
{
$message
=
$type
->
newMailMessageEngine
()
->
setMailer
(
$mailer
)
->
setMail
(
$this
)
->
setActors
(
$actors
)
->
setPreferences
(
$preferences
)
->
newMessage
(
$mailer
);
}
catch
(
Exception
$ex
)
{
$exceptions
[]
=
$ex
;
continue
;
}
if
(!
$message
)
{
// If we don't get a message back, that means the mail doesn't actually
// need to be sent (for example, because recipients have declined to
// receive the mail). Void it and return.
return
$this
->
setStatus
(
PhabricatorMailOutboundStatus
::
STATUS_VOID
)
->
save
();
}
try
{
$mailer
->
sendMessage
(
$message
);
}
catch
(
PhabricatorMetaMTAPermanentFailureException
$ex
)
{
// If any mailer raises a permanent failure, stop trying to send the
// mail with other mailers.
$this
->
setStatus
(
PhabricatorMailOutboundStatus
::
STATUS_FAIL
)
->
setMessage
(
$ex
->
getMessage
())
->
save
();
throw
$ex
;
}
catch
(
Exception
$ex
)
{
$exceptions
[]
=
$ex
;
continue
;
}
// Keep track of which mailer actually ended up accepting the message.
$mailer_key
=
$mailer
->
getKey
();
if
(
$mailer_key
!==
null
)
{
$this
->
setParam
(
'mailer.key'
,
$mailer_key
);
}
// Now that we sent the message, store the final deliverability outcomes
// and reasoning so we can explain why things happened the way they did.
$actor_list
=
array
();
foreach
(
$actors
as
$actor
)
{
$actor_list
[
$actor
->
getPHID
()]
=
array
(
'deliverable'
=>
$actor
->
isDeliverable
(),
'reasons'
=>
$actor
->
getDeliverabilityReasons
(),
);
}
$this
->
setParam
(
'actors.sent'
,
$actor_list
);
$this
->
setParam
(
'routing.sent'
,
$this
->
getParam
(
'routing'
));
$this
->
setParam
(
'routingmap.sent'
,
$this
->
getRoutingRuleMap
());
return
$this
->
setStatus
(
PhabricatorMailOutboundStatus
::
STATUS_SENT
)
->
save
();
}
// If we make it here, no mailer could send the mail but no mailer failed
// permanently either. We update the error message for the mail, but leave
// it in the current status (usually, STATUS_QUEUE) and try again later.
$messages
=
array
();
foreach
(
$exceptions
as
$ex
)
{
$messages
[]
=
$ex
->
getMessage
();
}
$messages
=
implode
(
"
\n\n
"
,
$messages
);
$this
->
setMessage
(
$messages
)
->
save
();
if
(
count
(
$exceptions
)
===
1
)
{
throw
head
(
$exceptions
);
}
throw
new
PhutilAggregateException
(
pht
(
'Encountered multiple exceptions while transmitting mail.'
),
$exceptions
);
}
public
static
function
shouldMailEachRecipient
()
{
return
PhabricatorEnv
::
getEnvConfig
(
'metamta.one-mail-per-recipient'
);
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted.
*
* Note that this expands recipients into their members, because delivery
* is never directly attempted to aggregate actors like projects.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public
function
buildRecipientList
()
{
$actors
=
$this
->
loadAllActors
();
$actors
=
$this
->
filterDeliverableActors
(
$actors
);
return
mpull
(
$actors
,
'getPHID'
);
}
public
function
loadAllActors
()
{
$actor_phids
=
$this
->
getExpandedRecipientPHIDs
();
return
$this
->
loadActors
(
$actor_phids
);
}
public
function
getExpandedRecipientPHIDs
()
{
$actor_phids
=
$this
->
getAllActorPHIDs
();
return
$this
->
expandRecipients
(
$actor_phids
);
}
private
function
getAllActorPHIDs
()
{
return
array_merge
(
array
(
$this
->
getParam
(
'from'
)),
$this
->
getToPHIDs
(),
$this
->
getCcPHIDs
());
}
/**
* Expand a list of recipient PHIDs (possibly including aggregate recipients
* like projects) into a deaggregated list of individual recipient PHIDs.
* For example, this will expand project PHIDs into a list of the project's
* members.
*
* @param list<phid> $phids List of recipient PHIDs, possibly including
* aggregate recipients.
* @return list<phid> Deaggregated list of mailable recipients.
*/
public
function
expandRecipients
(
array
$phids
)
{
if
(
$this
->
recipientExpansionMap
===
null
)
{
$all_phids
=
$this
->
getAllActorPHIDs
();
$this
->
recipientExpansionMap
=
id
(
new
PhabricatorMetaMTAMemberQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withPHIDs
(
$all_phids
)
->
execute
();
}
$results
=
array
();
foreach
(
$phids
as
$phid
)
{
foreach
(
$this
->
recipientExpansionMap
[
$phid
]
as
$recipient_phid
)
{
$results
[
$recipient_phid
]
=
$recipient_phid
;
}
}
return
array_keys
(
$results
);
}
private
function
filterDeliverableActors
(
array
$actors
)
{
assert_instances_of
(
$actors
,
'PhabricatorMetaMTAActor'
);
$deliverable_actors
=
array
();
foreach
(
$actors
as
$phid
=>
$actor
)
{
if
(
$actor
->
isDeliverable
())
{
$deliverable_actors
[
$phid
]
=
$actor
;
}
}
return
$deliverable_actors
;
}
private
function
loadActors
(
array
$actor_phids
)
{
$actor_phids
=
array_filter
(
$actor_phids
);
$viewer
=
PhabricatorUser
::
getOmnipotentUser
();
$actors
=
id
(
new
PhabricatorMetaMTAActorQuery
())
->
setViewer
(
$viewer
)
->
withPHIDs
(
$actor_phids
)
->
execute
();
if
(!
$actors
)
{
return
array
();
}
if
(
$this
->
getForceDelivery
())
{
// If we're forcing delivery, skip all the opt-out checks. We don't
// bother annotating reasoning on the mail in this case because it should
// always be obvious why the mail hit this rule (e.g., it is a password
// reset mail).
foreach
(
$actors
as
$actor
)
{
$actor
->
setDeliverable
(
PhabricatorMetaMTAActor
::
REASON_FORCE
);
}
return
$actors
;
}
// Exclude explicit recipients.
foreach
(
$this
->
getExcludeMailRecipientPHIDs
()
as
$phid
)
{
$actor
=
idx
(
$actors
,
$phid
);
if
(!
$actor
)
{
continue
;
}
$actor
->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_RESPONSE
);
}
// Before running more rules, save a list of the actors who were
// deliverable before we started running preference-based rules. This stops
// us from trying to send mail to disabled users just because a Herald rule
// added them, for example.
$deliverable
=
array
();
foreach
(
$actors
as
$phid
=>
$actor
)
{
if
(
$actor
->
isDeliverable
())
{
$deliverable
[]
=
$phid
;
}
}
// Exclude muted recipients. We're doing this after saving deliverability
// so that Herald "Send me an email" actions can still punch through a
// mute.
foreach
(
$this
->
getMutedPHIDs
()
as
$muted_phid
)
{
$muted_actor
=
idx
(
$actors
,
$muted_phid
);
if
(!
$muted_actor
)
{
continue
;
}
$muted_actor
->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_MUTED
);
}
// For the rest of the rules, order matters. We're going to run all the
// possible rules in order from weakest to strongest, and let the strongest
// matching rule win. The weaker rules leave annotations behind which help
// users understand why the mail was routed the way it was.
// Exclude the actor if their preferences are set.
$from_phid
=
$this
->
getParam
(
'from'
);
$from_actor
=
idx
(
$actors
,
$from_phid
);
if
(
$from_actor
)
{
$from_user
=
id
(
new
PhabricatorPeopleQuery
())
->
setViewer
(
$viewer
)
->
withPHIDs
(
array
(
$from_phid
))
->
needUserSettings
(
true
)
->
execute
();
$from_user
=
head
(
$from_user
);
if
(
$from_user
)
{
$pref_key
=
PhabricatorEmailSelfActionsSetting
::
SETTINGKEY
;
$exclude_self
=
$from_user
->
getUserSetting
(
$pref_key
);
if
(
$exclude_self
)
{
$from_actor
->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_SELF
);
}
}
}
$all_prefs
=
id
(
new
PhabricatorUserPreferencesQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withUserPHIDs
(
$actor_phids
)
->
needSyntheticPreferences
(
true
)
->
execute
();
$all_prefs
=
mpull
(
$all_prefs
,
null
,
'getUserPHID'
);
$value_email
=
PhabricatorEmailTagsSetting
::
VALUE_EMAIL
;
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags
=
$this
->
getParam
(
'mailtags'
);
if
(
$tags
)
{
foreach
(
$all_prefs
as
$phid
=>
$prefs
)
{
$user_mailtags
=
$prefs
->
getSettingValue
(
PhabricatorEmailTagsSetting
::
SETTINGKEY
);
// The user must have elected to receive mail for at least one
// of the mailtags.
$send
=
false
;
foreach
(
$tags
as
$tag
)
{
if
(((
int
)
idx
(
$user_mailtags
,
$tag
,
$value_email
))
==
$value_email
)
{
$send
=
true
;
break
;
}
}
if
(!
$send
)
{
$actors
[
$phid
]->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_MAILTAGS
);
}
}
}
foreach
(
$deliverable
as
$phid
)
{
switch
(
$this
->
getRoutingRule
(
$phid
))
{
case
PhabricatorMailRoutingRule
::
ROUTE_AS_NOTIFICATION
:
$actors
[
$phid
]->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_ROUTE_AS_NOTIFICATION
);
break
;
case
PhabricatorMailRoutingRule
::
ROUTE_AS_MAIL
:
$actors
[
$phid
]->
setDeliverable
(
PhabricatorMetaMTAActor
::
REASON_ROUTE_AS_MAIL
);
break
;
default
:
// No change.
break
;
}
}
// If recipients were initially deliverable and were added by "Send me an
// email" Herald rules, annotate them as such and make them deliverable
// again, overriding any changes made by the "self mail" and "mail tags"
// settings.
$force_recipients
=
$this
->
getForceHeraldMailRecipientPHIDs
();
$force_recipients
=
array_fuse
(
$force_recipients
);
if
(
$force_recipients
)
{
foreach
(
$deliverable
as
$phid
)
{
if
(
isset
(
$force_recipients
[
$phid
]))
{
$actors
[
$phid
]->
setDeliverable
(
PhabricatorMetaMTAActor
::
REASON_FORCE_HERALD
);
}
}
}
// Exclude recipients who don't want any mail. This rule is very strong
// and runs last.
foreach
(
$all_prefs
as
$phid
=>
$prefs
)
{
$exclude
=
$prefs
->
getSettingValue
(
PhabricatorEmailNotificationsSetting
::
SETTINGKEY
);
if
(
$exclude
)
{
$actors
[
$phid
]->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_MAIL_DISABLED
);
}
}
// Unless delivery was forced earlier (password resets, confirmation mail),
// never send mail to unverified addresses.
foreach
(
$actors
as
$phid
=>
$actor
)
{
if
(
$actor
->
getIsVerified
())
{
continue
;
}
$actor
->
setUndeliverable
(
PhabricatorMetaMTAActor
::
REASON_UNVERIFIED
);
}
return
$actors
;
}
public
function
getDeliveredHeaders
()
{
return
$this
->
getParam
(
'headers.sent'
);
}
public
function
setDeliveredHeaders
(
array
$headers
)
{
$headers
=
$this
->
flattenHeaders
(
$headers
);
return
$this
->
setParam
(
'headers.sent'
,
$headers
);
}
public
function
getUnfilteredHeaders
()
{
$unfiltered
=
$this
->
getParam
(
'headers.unfiltered'
);
if
(
$unfiltered
===
null
)
{
// Older versions of Phabricator did not filter headers, and thus did
// not record unfiltered headers. If we don't have unfiltered header
// data just return the delivered headers for compatibility.
return
$this
->
getDeliveredHeaders
();
}
return
$unfiltered
;
}
public
function
setUnfilteredHeaders
(
array
$headers
)
{
$headers
=
$this
->
flattenHeaders
(
$headers
);
return
$this
->
setParam
(
'headers.unfiltered'
,
$headers
);
}
private
function
flattenHeaders
(
array
$headers
)
{
assert_instances_of
(
$headers
,
'PhabricatorMailHeader'
);
$list
=
array
();
foreach
(
$list
as
$header
)
{
$list
[]
=
array
(
$header
->
getName
(),
$header
->
getValue
(),
);
}
return
$list
;
}
public
function
getDeliveredActors
()
{
return
$this
->
getParam
(
'actors.sent'
);
}
public
function
getDeliveredRoutingRules
()
{
return
$this
->
getParam
(
'routing.sent'
);
}
public
function
getDeliveredRoutingMap
()
{
return
$this
->
getParam
(
'routingmap.sent'
);
}
public
function
getDeliveredBody
()
{
return
$this
->
getParam
(
'body.sent'
);
}
public
function
setDeliveredBody
(
$body
)
{
return
$this
->
setParam
(
'body.sent'
,
$body
);
}
public
function
getURI
()
{
return
'/mail/detail/'
.
$this
->
getID
().
'/'
;
}
/* -( Routing )------------------------------------------------------------ */
public
function
addRoutingRule
(
$routing_rule
,
$phids
,
$reason_phid
)
{
$routing
=
$this
->
getParam
(
'routing'
,
array
());
$routing
[]
=
array
(
'routingRule'
=>
$routing_rule
,
'phids'
=>
$phids
,
'reasonPHID'
=>
$reason_phid
,
);
$this
->
setParam
(
'routing'
,
$routing
);
// Throw the routing map away so we rebuild it.
$this
->
routingMap
=
null
;
return
$this
;
}
private
function
getRoutingRule
(
$phid
)
{
$map
=
$this
->
getRoutingRuleMap
();
$info
=
idx
(
$map
,
$phid
,
idx
(
$map
,
'default'
));
if
(
$info
)
{
return
idx
(
$info
,
'rule'
);
}
return
null
;
}
private
function
getRoutingRuleMap
()
{
if
(
$this
->
routingMap
===
null
)
{
$map
=
array
();
$routing
=
$this
->
getParam
(
'routing'
,
array
());
foreach
(
$routing
as
$route
)
{
$phids
=
$route
[
'phids'
];
if
(
$phids
===
null
)
{
$phids
=
array
(
'default'
);
}
foreach
(
$phids
as
$phid
)
{
$new_rule
=
$route
[
'routingRule'
];
$current_rule
=
idx
(
$map
,
$phid
);
if
(
$current_rule
===
null
)
{
$is_stronger
=
true
;
}
else
{
$is_stronger
=
PhabricatorMailRoutingRule
::
isStrongerThan
(
$new_rule
,
$current_rule
);
}
if
(
$is_stronger
)
{
$map
[
$phid
]
=
array
(
'rule'
=>
$new_rule
,
'reason'
=>
$route
[
'reasonPHID'
],
);
}
}
}
$this
->
routingMap
=
$map
;
}
return
$this
->
routingMap
;
}
/* -( Preferences )-------------------------------------------------------- */
private
function
loadPreferences
(
$target_phid
)
{
$viewer
=
PhabricatorUser
::
getOmnipotentUser
();
if
(
self
::
shouldMailEachRecipient
())
{
$preferences
=
id
(
new
PhabricatorUserPreferencesQuery
())
->
setViewer
(
$viewer
)
->
withUserPHIDs
(
array
(
$target_phid
))
->
needSyntheticPreferences
(
true
)
->
executeOne
();
if
(
$preferences
)
{
return
$preferences
;
}
}
return
PhabricatorUserPreferences
::
loadGlobalPreferences
(
$viewer
);
}
public
function
shouldRenderMailStampsInBody
(
$viewer
)
{
$preferences
=
$this
->
loadPreferences
(
$viewer
->
getPHID
());
$value
=
$preferences
->
getSettingValue
(
PhabricatorEmailStampsSetting
::
SETTINGKEY
);
return
(
$value
==
PhabricatorEmailStampsSetting
::
VALUE_BODY_STAMPS
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public
function
getCapabilities
()
{
return
array
(
PhabricatorPolicyCapability
::
CAN_VIEW
,
);
}
public
function
getPolicy
(
$capability
)
{
return
PhabricatorPolicies
::
POLICY_NOONE
;
}
public
function
hasAutomaticCapability
(
$capability
,
PhabricatorUser
$viewer
)
{
$actor_phids
=
$this
->
getExpandedRecipientPHIDs
();
return
in_array
(
$viewer
->
getPHID
(),
$actor_phids
);
}
public
function
describeAutomaticCapability
(
$capability
)
{
return
pht
(
'The mail sender and message recipients can always see the mail.'
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public
function
destroyObjectPermanently
(
PhabricatorDestructionEngine
$engine
)
{
$files
=
$this
->
loadAttachedFiles
(
$engine
->
getViewer
());
foreach
(
$files
as
$file
)
{
$engine
->
destroyObject
(
$file
);
}
$this
->
delete
();
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, May 12, 1:55 PM (2 d)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
120636
Default Alt Text
PhabricatorMetaMTAMail.php (35 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment