jdz / mailer
JDZ mailer
Requires
- php: >=8.2
- symfony/mailer: ^7.4
Requires (Dev)
- ph-7/html-to-text: ^2.0
- phpunit/phpunit: ^11.0
- soundasleep/html2text: ^2.0
- symfony/dotenv: ^7.4
- symfony/mailchimp-mailer: ^7.4
Suggests
- ph-7/html-to-text: To convert the html mail body to plain text with Pierre-Henry de Soria's Html2Text converter.
- soundasleep/html2text: To convert the html mail body to plain text with Sound Asleep Html2Text converter (2.x, for its char_set option).
- symfony/mailchimp-mailer: To send through Mailchimp Transactional (Mandrill), type mailchimp.
Provides
None
Conflicts
None
Replaces
None
README
A small facade over symfony/mailer: the same Mailer you configure with set() / setSMTP() / setMailchimp() / setContent(), sent through Symfony's transports — SMTP, sendmail, PHP's native mail, Mailchimp Transactional (Mandrill) — with HTML + plain-text alternatives, attachments, DKIM signing and an optional failover between transports.
Installation
composer require jdz/mailer
Optional:
composer require symfony/mailchimp-mailer # Mailchimp Transactional (type mailchimp) composer require ph-7/html-to-text # PH7 html-to-text converter composer require soundasleep/html2text # Soundasleep html-to-text converter
Requirements
- PHP >= 8.2
- symfony/mailer ^7.4 (installed with the package)
Configuration
Copy .env.dist to .env and fill in your credentials:
cp .env.dist .env
The .env file is gitignored and never committed.
Usage
SMTP Email
use Symfony\Component\Dotenv\Dotenv; use JDZ\Mailer\Mailer; use JDZ\Mailer\AltBody\BasicAltBody; (new Dotenv())->loadEnv(__DIR__ . '/.env'); $mail = new Mailer(); $mail->setProperties([ 'domain' => 'example.com', 'charset' => 'utf-8', ]); $mail->setContent([ 'isHtml' => true, 'content' => '<h1>Hello</h1><p>This is a test email.</p>', 'template' => '<html><body>{{BODY}}</body></html>', 'altBodyFormatter' => new BasicAltBody(), ]); $mail->setSMTP([ 'host' => $_ENV['SMTP_HOST'], 'port' => (int)$_ENV['SMTP_PORT'], 'secure' => $_ENV['SMTP_SECURE'], // tls (STARTTLS) or ssl (smtps) 'auth' => true, 'user' => $_ENV['SMTP_USER'], 'pass' => $_ENV['SMTP_PASS'], ]); $mail->setFrom($_ENV['MAIL_FROM_EMAIL'], $_ENV['MAIL_FROM_NAME']); $mail->addRecipient('recipient@example.com', 'Recipient'); $mail->set('subject', 'Hello from JDZ Mailer'); $mail->send();
localMode = true skips the TLS peer verification (a local SMTP with a self-signed certificate). The SMTP conversation of the send is in $mail->DebugInfos.
A symfony/mailer DSN
A DSN set on the mailer bypasses the type / smtp / mailchimp configs and is handed to Symfony as is:
$mail->dsn = $_ENV['MAILER_DSN']; // smtp://user:pass@host:587, sendmail://default, native://default, failover(...)...
Mailchimp Transactional
Needs symfony/mailchimp-mailer. The mail goes through Mandrill's SMTP endpoint (mandrill+smtp://), where every option below is a documented X-MC-* header; the tags become X-MC-Tags.
$mail = new Mailer(); $mail->setContent([ 'isHtml' => true, 'content' => '<p>Newsletter content</p>', 'altBodyFormatter' => new BasicAltBody(), ]); $mail->setMailchimp([ 'apiKey' => $_ENV['MAILCHIMP_API_KEY'], 'user' => 'My account', // the SMTP username, any string; the from address when empty 'track_opens' => true, 'track_clicks' => true, 'preserve_recipients' => true, 'auto_text' => true, 'auto_html' => false, ]); $mail->setFrom('news@example.com', 'News'); $mail->addRecipient('subscriber@example.com', 'Subscriber'); $mail->addTag('newsletter'); $mail->set('subject', 'Monthly Newsletter'); $mail->send();
A recipient Mandrill rejects is an SMTP error: a MailchimpException.
Native mail() and sendmail
With no SMTP nor Mailchimp config, the mail goes through Symfony's native://default transport (PHP's sendmail_path, or the ini SMTP settings on Windows). type = 'sendmail' uses sendmail://default, type = 'qmail' the qmail binary.
$mail = new Mailer(); $mail->domain = 'example.com'; $mail->setFrom('sender@example.com', 'Sender'); $mail->addRecipient('recipient@example.com'); $mail->set('subject', 'Simple email'); $mail->content->setProperties([ 'content' => 'Plain text message', 'altBodyFormatter' => new BasicAltBody(), ]); $mail->send();
Fallback
With useFallback, a config that fails its check rules its transport out at check() time (mailchimp → smtp → mail), and the transports that remain are chained in a Symfony failover() DSN: a transport that is down at send time falls through to the next one.
$mail->useFallback = true; // mailchimp -> smtp -> mail()
DKIM
$mail->setDKIM([ 'domain' => 'example.com', // defaults to the mailer's domain 'selector' => 'mail', 'private' => $privateKeyPem, 'passphrase' => '', ]);
The message is signed with Symfony\Component\Mime\Crypto\DkimSigner (rsa-sha256, relaxed/relaxed) before the SMTP, sendmail or native transport. Mandrill signs on its own side: a Mailchimp mail is not signed here. symfony/mime signs as the domain (i=@domain): the identity key is accepted but not used.
Recipients
$mail->addRecipient('to@example.com', 'To'); $mail->addCc('cc@example.com', 'CC'); $mail->addBcc('bcc@example.com', 'BCC'); $mail->addReplyTo('reply@example.com', 'Reply To'); // Or bulk: $mail->addRecipients([ ['email' => 'alice@example.com', 'name' => 'Alice'], ['email' => 'bob@example.com', 'name' => 'Bob'], ]); // No-reply (prevents further addReplyTo calls): $mail->setNoReply('noreply@example.com');
An address that is not RFC-compliant is a ConfigException at send().
Attachments
$mail->addAttachment('/path/to/file.pdf', 'report.pdf', '', 'application/pdf'); $mail->addAttachment('/path/to/logo.png', 'logo.png', '', 'image/png', 'inline'); // embedded
The name defaults to the file's; the encoding argument is accepted for compatibility and ignored (Symfony picks it). An HTML mail with a text alternative and attachments is a multipart/mixed holding a multipart/alternative.
Content Replacements
$mail->setContent([ 'isHtml' => true, 'content' => '<p>Hello {{NAME}}, your order #{{ORDER}} is ready.</p>', 'replacements' => [ '{{NAME}}' => 'John', '{{ORDER}}' => '12345', ], 'altBodyFormatter' => new BasicAltBody(), ]);
Template-Only Mode
Use a full template without separate content:
$mail->setContent([ 'templateOnly' => true, 'template' => '<html><body><h1>Full HTML template</h1></body></html>', 'altBodyFormatter' => new BasicAltBody(), ]);
Alt Body Formatters
Three HTML-to-plain-text converters are available:
BasicAltBody- Built-in, usesstrip_tags()(no extra dependency)Ph7AltBody- Usesph-7/html-to-textSoundasleepAltBody- Usessoundasleep/html2text
Exceptions
ConfigException- Invalid configuration (missing fields, bad values, an invalid address, an unreadable DKIM key)SmtpException- An SMTP, sendmail or native transport failure (Symfony'sTransportExceptionInterface, available throughgetPrevious())MailchimpException- A Mandrill failure, a rejected recipient includedException- Anything else (an unreadable attachment...)
try { $mail->send(); } catch (\JDZ\Mailer\Exception\ConfigException $e) { // Bad configuration } catch (\JDZ\Mailer\Exception\SmtpException $e) { // Transport error } catch (\JDZ\Mailer\Exception\MailchimpException $e) { // Mailchimp error } catch (\JDZ\Mailer\Exception\Exception $e) { // General error }
Upgrading to 2.0
Nothing changes in a consumer that only uses the facade: every 1.x property, setter, config key and exception is still there. What is different underneath:
symfony/maileris required;phpmailer/phpmailerandmailchimp/transactionalare not used any more (composer removethem if nothing else needs them). Mailchimp needssymfony/mailchimp-mailer.check()messages: "Mailchimp needs an API key and symfony/mailchimp-mailer" and "SMTP needs a complete config (host, and user + pass when auth is on)" replace the two "... is required" ones.SmtpExceptionis thrown (the README used to promise it;Exceptionwas thrown).- Mailchimp goes through Mandrill's SMTP endpoint:
mailchimp.useris the SMTP username (optional). - The MIME is Symfony's: the text alternative is nested in the mixed part with the attachments (mail clients showed both versions), the subject is RFC 2047-encoded,
DateandMessage-IDare generated when not set,X-Priority: 1only on an important mail. DebugInfosholds the SMTP conversation Symfony reports.- DKIM signs as the domain (
i=@domain); theidentitykey is not used. type = 'sendmail'/'qmail'are real sendmail transports (they used to go throughmail()).languageis kept and unused.
Changelog
- 2.0.0 (2026-10-11) — on symfony/mailer:
DsnBuilder(configs → DSN,failover()withuseFallback, adsnproperty to bypass the configs),EmailBuilder(facade →Email, MandrillX-MC-*headers, DKIM throughDkimSigner);SmtpExceptionthrown on a transport failure, a rejected Mandrill recipient is aMailchimpException; the senders, PHPMailer, the Mailchimp API client and the hand-rolled MIME are gone. See Upgrading to 2.0.
Testing
composer test # or vendor/bin/phpunit
License
This project is licensed under the MIT License - see the LICENSE file for details.