Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions mailer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,173 @@
}
}

PGP/MIME Signing and Encryption
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 8.2

PGP/MIME signing and encryption were introduced in Symfony 8.2.

.. warning::

The PGP/MIME classes are marked as ``@experimental``, so they might change
without prior notice in future Symfony versions.

Messages can also be signed and encrypted with `PGP/MIME`_. Unlike S/MIME, this
doesn't rely on the OpenSSL PHP extension: it runs the ``gpg`` binary through the
:doc:`Process component </components/process>`, so both must be available on the
machine that sends the emails. Keys are given as paths to ASCII armored files.

.. warning::

Only the body of the message is protected. Headers such as ``Subject``,
``From`` and ``To`` are still sent in cleartext, which is inherent to
PGP/MIME. Don't assume that the subject of an encrypted email is private.

Use the :class:`Symfony\\Component\\Mime\\Crypto\\PgpSigner` and
:class:`Symfony\\Component\\Mime\\Crypto\\PgpEncrypter` classes to sign and
encrypt a message by yourself::

use Symfony\Component\Mime\Crypto\PgpEncrypter;

Check failure on line 1942 in mailer.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\Mime\Crypto\PgpEncrypter" does not exist (in mailer.rst on line 1942)
use Symfony\Component\Mime\Crypto\PgpSigner;

Check failure on line 1943 in mailer.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\Mime\Crypto\PgpSigner" does not exist (in mailer.rst on line 1943)
use Symfony\Component\Mime\Email;

$email = new Email()
->from('hello@example.com')
->to('alice@example.com')
// ...
->html('...');

// the second argument is optional; when given, that public key is attached to
// the message and included in the signed content (signing always uses the
// secret key). The third argument is the passphrase of the secret key
$signer = new PgpSigner('/path/to/secret-key.asc', '/path/to/public-key.asc', 'the-passphrase');
$signedEmail = $signer->sign($email);

// recipient keys are passed to encrypt() and not to the constructor, so the
// same encrypter can be reused for messages sent to different recipients
$encrypter = new PgpEncrypter();
$encryptedEmail = $encrypter->encrypt($signedEmail, [
// key = recipient email address; value = path to their public key file
'alice@example.com' => '/path/to/alice.asc',
]);

// now use the Mailer component to send this $encryptedEmail instead of the original email

Both classes accept an array of options as their last argument to define the
``binary`` path of ``gpg``, its ``timeout``, the ``digest_algorithm`` used when
signing and the ``cipher_algorithm`` used when encrypting.

Signing and Encrypting Messages Globally with PGP/MIME
......................................................

Instead of signing and encrypting each message by yourself, configure the
``pgp_signer`` and ``pgp_encrypter`` options to do it for the messages that you
flag with the ``X-Pgp-Sign`` and ``X-Pgp-Encrypt`` headers (as explained below):

.. configuration-block::

.. code-block:: yaml

# config/packages/mailer.yaml
framework:
mailer:
pgp_signer:
enabled: true
secret_key: '%kernel.project_dir%/config/keys/private.asc'
passphrase: '%env(PGP_PASSPHRASE)%'
# SHA224, SHA256, SHA384 or SHA512 (default)
digest_algorithm: 'SHA512'
pgp_encrypter:
enabled: true
# define the recipient public keys explicitly...
keys:
'alice@example.com': '%kernel.project_dir%/config/keys/alice.asc'
# ...or get them from a service (you can't use both options at the same time)
# repository: App\Pgp\PublicKeyRepository
on_missing_key: 'fail'
cipher_algorithm: 'AES256'

Check failure on line 2000 in mailer.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Cache Warmup] In ArrayNode.php line 312: Unrecognized options "pgp_signer, pgp_encrypter" under "framework.mailer". Available options are "dkim_signer", "dsn", "enabled", "envelope", "headers ", "message_bus", "smime_encrypter", "smime_signer", "transports". (in mailer.rst on line 2000)

.. code-block:: php

// config/packages/mailer.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

// use App\Pgp\PublicKeyRepository;

return App::config([
'framework' => [
'mailer' => [
'pgp_signer' => [
'enabled' => true,
'secret_key' => '%kernel.project_dir%/config/keys/private.asc',
'passphrase' => env('PGP_PASSPHRASE'),
// SHA224, SHA256, SHA384 or SHA512 (default)
'digest_algorithm' => 'SHA512',
],
'pgp_encrypter' => [
'enabled' => true,
// define the recipient public keys explicitly...
'keys' => [
'alice@example.com' => '%kernel.project_dir%/config/keys/alice.asc',
],
// ...or get them from a service (you can't use both options at the same time)
// 'repository' => PublicKeyRepository::class,
'on_missing_key' => 'fail',
'cipher_algorithm' => 'AES256',
],
],
],
]);

Check failure on line 2032 in mailer.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Cache Warmup] In ArrayNode.php line 312: Unrecognized options "pgp_signer, pgp_encrypter" under "framework.mailer". Available options are "dkim_signer", "dsn", "enabled", "envelope", "headers ", "message_bus", "smime_encrypter", "smime_signer", "transports". (in mailer.rst on line 2032)

The ``repository`` option is the ID of a service implementing
:class:`Symfony\\Component\\Mailer\\EventListener\\PgpPublicKeyRepositoryInterface`,
whose only method (``findPublicKeyPathFor()``) returns the path to the public key
of the given email address, or ``null`` when there's none.

Unlike the S/MIME signer, PGP/MIME is not applied to every message: add the
``X-Pgp-Sign`` and/or ``X-Pgp-Encrypt`` headers to select the messages to
protect::

$email->getHeaders()->addTextHeader('X-Pgp-Sign', 'true');
$email->getHeaders()->addTextHeader('X-Pgp-Encrypt', 'true');

Both headers are removed from the message before sending it. When both are used,
the message is signed first and then encrypted, and this happens after the
contents of :ref:`templated emails <mailer-twig>` are rendered and before the
message is logged, so its plaintext is never written to the logs.

The ``on_missing_key`` option defines what to do when some recipient has no
public key:

``fail`` (default)
Throw a :class:`Symfony\\Component\\Mime\\Exception\\KeyNotFoundException`
naming every recipient without a key;
``encrypt``
Encrypt for the recipients that have a key; the others still receive the
message, but they can't read it;
``skip``
Encrypt for the recipients that have a key and remove the others from the
envelope.

Whatever the mode, the message is never sent unencrypted: when no recipient at
all has a key, an exception is always thrown. Set the value of the
``X-Pgp-Encrypt`` header to ``fail``, ``encrypt`` or ``skip`` to override the
configured mode for a single message.

.. note::

The recipients dropped by the ``skip`` mode are removed from the envelope,
but the ``framework.mailer.envelope.recipients`` option is applied afterwards
and overrides that list.

Two other options control who can read the message: enable ``encrypt_for_sender``
to also encrypt it for the sender, so it can read the messages it sent (this is
disabled by default because it widens the number of people able to decrypt the
message); and enable ``hide_recipients`` to hide the key IDs of all the
recipients in the encrypted message. The recipients listed in the ``Bcc`` header
are always hidden, so the ciphertext doesn't leak the blind copy list.

.. _multiple-email-transports:

Multiple Email Transports
Expand Down Expand Up @@ -2650,6 +2817,7 @@
.. _`Resend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Resend/README.md
.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt
.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME
.. _`PGP/MIME`: https://datatracker.ietf.org/doc/html/rfc3156
.. _`Scaleway`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Scaleway/README.md
.. _`SendGrid`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md
.. _`MJML`: https://github.com/mjmlio/mjml
Expand Down
141 changes: 141 additions & 0 deletions reference/configuration/framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2906,6 +2906,147 @@ cipher

The `OpenSSL cipher`_ algorithm constant used for encryption.

pgp_signer
..........

.. versionadded:: 8.2

The ``pgp_signer`` option was introduced in Symfony 8.2.

Configures a global PGP/MIME signer that signs the outgoing messages having the
``X-Pgp-Sign`` header.

enabled
"""""""

**type**: ``boolean`` **default**: ``false``

Whether to enable the PGP/MIME signer.

secret_key
""""""""""

**type**: ``string`` **default**: ``''``

The path to the secret key of the sender, in ASCII armored format (without the
``file://`` prefix). It is required when the signer is enabled.

public_key
""""""""""

**type**: ``string`` **default**: ``null``

The path to the public key of the sender, in ASCII armored format. When set,
that key is attached to the message and included in the signed content.

passphrase
""""""""""

**type**: ``string`` **default**: ``null``

The passphrase of the secret key.

binary
""""""

**type**: ``string`` **default**: ``'gpg'``

The path to the GnuPG binary.

digest_algorithm
""""""""""""""""

**type**: ``string`` **default**: ``'SHA512'``

The digest algorithm used to sign the message. Allowed values are ``SHA224``,
``SHA256``, ``SHA384`` and ``SHA512``.

pgp_encrypter
.............

.. versionadded:: 8.2

The ``pgp_encrypter`` option was introduced in Symfony 8.2.

Configures a global PGP/MIME encrypter that encrypts the outgoing messages having
the ``X-Pgp-Encrypt`` header.

enabled
"""""""

**type**: ``boolean`` **default**: ``false``

Whether to enable the PGP/MIME encrypter.

repository
""""""""""

**type**: ``string`` **default**: ``''``

The service ID of a class implementing
:class:`Symfony\\Component\\Mailer\\EventListener\\PgpPublicKeyRepositoryInterface`.
This service is used to find the public key path for each email recipient. It
can't be used together with the ``keys`` option, and one of them is required
when the encrypter is enabled.

keys
""""

**type**: ``array`` **default**: ``[]``

The public keys of the recipients, as a map of email address to public key file
path. It can't be used together with the ``repository`` option.

binary
""""""

**type**: ``string`` **default**: ``'gpg'``

The path to the GnuPG binary.

cipher_algorithm
""""""""""""""""

**type**: ``string`` **default**: ``'AES256'``

The cipher algorithm used to encrypt the message. Allowed values are ``AES``,
``AES192``, ``AES256``, ``TWOFISH``, ``CAMELLIA128``, ``CAMELLIA192`` and
``CAMELLIA256``.

timeout
"""""""

**type**: ``float`` **default**: ``60.0``

The timeout in seconds of the ``gpg`` process.

hide_recipients
"""""""""""""""

**type**: ``boolean`` **default**: ``false``

Whether to hide the key IDs of all the recipients in the encrypted message. The
recipients listed in the ``Bcc`` header are always hidden, whatever the value of
this option.

on_missing_key
""""""""""""""

**type**: ``string`` **default**: ``'fail'``

The behavior when a recipient has no public key: ``fail`` throws an exception,
``encrypt`` encrypts for the recipients that have a key (the others receive an
unreadable message) and ``skip`` also drops the keyless recipients from the
envelope. The message is never sent unencrypted.

encrypt_for_sender
""""""""""""""""""

**type**: ``boolean`` **default**: ``false``

Whether to also encrypt the message for the sender, when a public key is
available for its address, so that the sender can read the messages it sent.

messenger
~~~~~~~~~

Expand Down
Loading