mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Compose: allow a send addressed to Bcc only (#200)
A send with nobody in To was refused with "Add at least one recipient before sending", even with addresses in Cc or Bcc. The window's guard counted To, Cc and Bcc; the server's Count(min: 1) rule in the send group counted To only. The rule is now a Callback on the To field that counts all three, read off the bound form since the fields are unmapped. A send with no recipient at all is still refused; a draft may still be saved with none. Closes #201. Co-Authored-By: jo2904 <josephdegaullier@gmx.fr> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+80
-4
No files matched your search
@@ -8,6 +8,11 @@ The published image tags: `latest` follows the most recent release below,
|
||||
|
||||
## Unreleased
|
||||
|
||||
**A mail can go to Bcc recipients only.** Sending with nobody in To was refused with "Add at least
|
||||
one recipient before sending", even with addresses in Cc or Bcc: the window counted all three, but
|
||||
the server only counted To. It now counts all three as well. A mail with no recipient at all is
|
||||
still refused, and a draft may still be saved with none. No schema change.
|
||||
|
||||
**Outlook's `<![endif]-->` no longer shows up in a message.** Mail written for Outlook hides
|
||||
parts of itself behind conditional comments, and one form of them ends in a marker that is not
|
||||
inside a comment. A mail that also had a comment containing `--` anywhere in it was read by an older
|
||||
|
||||
@@ -21,7 +21,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Validator\Constraints\Count;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class ComposeType extends AbstractType
|
||||
{
|
||||
@@ -70,12 +72,17 @@ class ComposeType extends AbstractType
|
||||
// ContactAutocompleteField, which drops what it cannot validate).
|
||||
// "No valid recipient" and "no recipient" are therefore the same
|
||||
// failure by the time the form is bound, and one rule covers both.
|
||||
//
|
||||
// Counted across To, Cc and Bcc, as the window's own guard does: a
|
||||
// mail sent only to Bcc is a normal thing to want (a list kept
|
||||
// private, a copy to oneself), and SMTP, Gmail and Graph all take
|
||||
// it. The rule sits on To so the refusal shows where the user looks
|
||||
// first.
|
||||
->add('toAddresses', ContactAutocompleteField::class, [
|
||||
'mapped' => false,
|
||||
'constraints' => [
|
||||
new Count(
|
||||
min: 1,
|
||||
minMessage: 'compose.recipient_required',
|
||||
new Callback(
|
||||
callback: self::requireARecipient(...),
|
||||
groups: ['send'],
|
||||
),
|
||||
],
|
||||
@@ -270,6 +277,34 @@ class ComposeType extends AbstractType
|
||||
return '' !== $name ? sprintf('%s <%s>', $name, $address) : $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse a send with nobody in To, Cc or Bcc.
|
||||
*
|
||||
* Read off the bound form rather than off the Message: the three fields are
|
||||
* unmapped, and the controller copies them onto the entity only after
|
||||
* validation has run.
|
||||
*/
|
||||
private static function requireARecipient(mixed $to, ExecutionContextInterface $context): void
|
||||
{
|
||||
$field = $context->getObject();
|
||||
|
||||
if (false === $field instanceof FormInterface || null === $field->getParent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form = $field->getParent();
|
||||
|
||||
foreach (['toAddresses', 'ccAddresses', 'bccAddresses'] as $name) {
|
||||
$selected = $form->get($name)->getData();
|
||||
|
||||
if (true === is_countable($selected) && count($selected) > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$context->buildViolation('compose.recipient_required')->addViolation();
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
|
||||
@@ -165,6 +165,42 @@ final class ComposeSafetyTest extends WebTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bcc alone is a recipient.
|
||||
*
|
||||
* The rule used to count To only, so a mail meant for Bcc recipients and
|
||||
* nobody else came back with "Add at least one recipient", although the
|
||||
* window itself had let it through.
|
||||
*/
|
||||
public function testASendToBccAloneIsAccepted(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = $this->boot($client);
|
||||
|
||||
$account = $this->account($user, 'sender@joder.dev', sortOrder: 0, primary: true);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $client->request('GET', '/compose/new', server: self::DOCK_FRAME);
|
||||
$token = $crawler->filter('input[name="compose[_token]"]')->attr('value');
|
||||
|
||||
$client->request('POST', '/compose/send', [
|
||||
'compose' => [
|
||||
'_token' => $token,
|
||||
'account' => $account->id . '|sender@joder.dev',
|
||||
'bccAddresses' => ['hidden@example.test'],
|
||||
'subject' => 'Bcc only',
|
||||
'bodyHtml' => '<p>Body</p>',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringNotContainsString(
|
||||
'add at least one recipient',
|
||||
strtolower((string) $client->getResponse()->getContent()),
|
||||
);
|
||||
}
|
||||
|
||||
/** A draft, by contrast, may have no recipient at all. */
|
||||
public function testADraftMayStillBeSavedWithNoRecipient(): void
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user