PHP powers roughly 77% of all websites (WordPress, Laravel, Symfony, Magento), so sending email from a PHP script is something almost every developer will need to do at some point.
This guide walks through every practical method: the native mail() function and why it falls short, PHPMailer with an SMTP server, the Brevo SDK for REST API calls, raw cURL when you can't use Composer, and framework integrations. Each approach includes working code you can drop into a project.
Table of Contents
- Three ways to send email in PHP
- Method 1: the PHP mail() function
- Method 2: PHPMailer + Brevo SMTP relay
- Method 3: Brevo PHP SDK v4 (REST API)
- Method 4: raw cURL (no Composer, no SDK)
- Laravel integration
- Legacy v2 SDK (PHP 7.x / WordPress)
- Sending personalized emails at scale
- Security best practices
- Quick reference: which method to use
- What's next
- FAQ
Three ways to send email in PHP
Sending emails in PHP breaks down into three categories. They differ quite a bit in setup effort, features, and how reliably your messages actually get delivered.
- The PHP mail() function is built in with zero dependencies, but it's not really suited for production. There's no SMTP authentication, error handling is minimal, and deliverability is poor.
- Email libraries like PHPMailer or Symfony Mailer are the standard choice for professional PHP apps. They bridge your code and a real mail server, with support for SMTP authentication, SSL/TLS encryption, HTML templates, and attachments.
- Email APIs (REST) are the strongest option for transactional email. Providers like Brevo offer RESTful APIs with delivery analytics, open/click tracking, bounce management, and dedicated IP pools.
Method 1: the PHP mail() function
PHP's built-in mail() function lets you send email from a script with nothing else installed. It's worth understanding even if you plan to use something better, because it shows up in legacy codebases constantly. Knowing where it breaks helps you make the right call when modernizing.
Syntax and parameters
The mail() function takes three required parameters (recipient address, subject, message body) plus optional headers and extra parameters:
mail(to, subject, message, headers, parameters);
- to is the recipient email address, or a comma-separated list for multiple recipients
- subject is the email subject line
- message is the body text
- headers covers additional headers like From, Cc, Reply-To, MIME-Version, and Content-type
- parameters gets passed to the sendmail program (for example, -f sets the envelope sender)
Send a plain text email
The simplest possible mail() call, a plain text email with a From header:
<?php
$to = '[email protected]';
$subject = 'Your subject line';
$message = 'Hello, this is a plain text email sent from PHP.';
$headers = 'From: [email protected]' . "\r\n" .
'Reply-To: [email protected]' . "\r\n" .
'X-Mailer: PHP/' . phpversion();
if (mail($to, $subject, $message, $headers)) {
echo 'Message sent successfully.';
} else {
echo 'Message could not be sent.';
}
Use wordwrap() on long messages to keep lines under 70 characters, as required by RFC 2822:
$message = wordwrap($message, 70, "\r\n");
Send an HTML email with mail()
You can send HTML through mail(), but you need the right headers. Set MIME-Version and Content-type, or email clients will display raw HTML tags as plain text.
<?php
$to = '[email protected]';
$subject = 'Order Confirmation';
$html_message = '
<html>
<head><title>Order Confirmation</title></head>
<body>
<h1>Your order is confirmed</h1>
<p>Order <strong>#12345</strong> has been received and is being processed.</p>
</body>
</html>';
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=UTF-8' . "\r\n";
$headers .= 'From: [email protected]' . "\r\n";
$headers .= 'Reply-To: [email protected]' . "\r\n";
if (mail($to, $subject, $html_message, $headers)) {
echo 'HTML email sent successfully.';
} else {
echo 'Message could not be sent.';
}
If you're sending HTML, always include Content-type: text/html; charset=UTF-8. It's also good practice to provide a plain text alternative, since spam filters tend to flag HTML-only emails. The mail() function doesn't handle multipart emails on its own, which is one of its bigger limitations.
Send to multiple recipients
To send the same email to several people, pass a comma-separated string as the to parameter:
<?php
$to = '[email protected], [email protected], [email protected]';
$subject = 'Team update';
$message = 'The release is live. Please review the deployment notes.';
$headers = 'From: [email protected]' . "\r\n" .
'Reply-To: [email protected]' . "\r\n";
if (mail($to, $subject, $message, $headers)) {
echo 'Message sent to all recipients.';
} else {
echo 'Message could not be sent.';
}
Keep in mind that mail() opens and closes an SMTP socket for each individual email, so it gets slow with large batches and can trigger rate limiting on the server side.
Send an email with an attachment using mail()
To attach a file with mail(), you need to set Content-type to multipart/mixed, define MIME boundaries, and base64-encode the attachment:
<?php
$to = '[email protected]';
$subject = 'Your invoice';
$from = '[email protected]';
$file = '/path/to/invoice.pdf';
$filename = basename($file);
$filedata = chunk_split(base64_encode(file_get_contents($file)));
$mimetype = 'application/pdf';
$boundary = md5(time());
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: multipart/mixed; boundary="' . $boundary . '"' . "\r\n";
$headers .= 'From: ' . $from . "\r\n";
$headers .= 'Reply-To: ' . $from . "\r\n";
$message = '--' . $boundary . "\r\n";
$message .= 'Content-type: text/plain; charset=UTF-8' . "\r\n";
$message .= 'Content-transfer-encoding: 7bit' . "\r\n\r\n";
$message .= 'Please find your invoice attached.' . "\r\n\r\n";
$message .= '--' . $boundary . "\r\n";
$message .= 'Content-type: ' . $mimetype . '; name="' . $filename . '"' . "\r\n";
$message .= 'Content-transfer-encoding: base64' . "\r\n";
$message .= 'Content-disposition: attachment; filename="' . $filename . '"' . "\r\n\r\n";
$message .= $filedata . "\r\n";
$message .= '--' . $boundary . '--';
if (mail($to, $subject, $message, $headers)) {
echo 'Email with attachment sent.';
} else {
echo 'Message could not be sent.';
}
This requires careful construction of headers and MIME boundaries. It's fiddly and error-prone, which is one more reason most developers reach for PHPMailer instead.
PHP mail() limitations
The mail() function works for quick tests and local development, but it has real problems in production:
- It doesn't support SMTP authentication, so emails are more likely to be flagged as spam or blocked outright by ISPs.
- Deliverability is poor. Emails sent directly from a web server without SPF, DKIM, or DMARC records will often land in spam or get rejected. mail() gives you no control over any of this.
- Error reporting is basically nonexistent. mail() returns true when the local MTA accepts the message, not when it's actually delivered. Silent failures happen all the time.
- You can't connect to an external SMTP server without modifying php.ini, and even then the options are limited.
- It's inefficient for bulk sending because it opens and closes an SMTP socket per email, which can hit rate limits fast.
- There's no built-in multipart support. Sending HTML with a plain text fallback means manually constructing raw MIME headers, and getting that right is tedious.
Configuring mail() via php.ini
The mail() function often won't work on local setups (XAMPP, WAMP) without configuring SMTP in php.ini.
On Windows, point the SMTP parameter to your mail server and set sendmail_from:
On Linux/Mac, set the sendmail_path parameter:
sendmail_path = /usr/sbin/sendmail -t -i
Restart PHP-FPM or Apache after making changes.
Even with php.ini configured, using mail() through a local mail server is unreliable for production. PHPMailer with an external SMTP server is more reliable, more secure, and gives you actual error reporting.
Method 2: PHPMailer + Brevo SMTP relay
PHPMailer is the most widely used library for sending emails from PHP. WordPress, Drupal, Joomla, and most major PHP frameworks rely on it, and it fixes every limitation of the native mail() function. It handles SMTP authentication, SSL/TLS encryption, HTML templates, and attachments without any extra work on your end.
Pair it with Brevo's SMTP server and you get full deliverability (DKIM, SPF, bounce tracking) without touching the Brevo API directly.
SMTP credentials
Before writing any code:
- Verify your sender address in Brevo under Settings > Senders & IPs > Senders. Without this, all emails will fail.
- Generate an SMTP key under Settings > SMTP & API > SMTP Keys tab. This is a separate credential from your API key. Using the wrong one is the most common authentication failure.
SMTP server settings:
The From address must belong to a domain you own and have configured for sending (SPF/DKIM). Some hosting providers also limit how many emails you can send per hour, so check your plan.
Migrating from Sendinblue? Update your host from smtp.sendinblue.com to smtp-relay.brevo.com.
Install PHPMailer
composer require phpmailer/phpmailer
If you can't use Composer, download PHPMailer as a zip, extract it into one of your include_path directories, and load each class file manually:
require 'path/to/PHPMailer/src/Exception.php';
require 'path/to/PHPMailer/src/PHPMailer.php';
require 'path/to/PHPMailer/src/SMTP.php';
Send a plain text email with PHPMailer
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'smtp-relay.brevo.com';
$mail->SMTPAuth = true;
$mail->Username = '[email protected]';
$mail->Password = 'YOUR_SMTP_KEY';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('[email protected]', 'My App');
$mail->addAddress('[email protected]', 'John Doe');
$mail->addReplyTo('[email protected]', 'Support');
$mail->Subject = 'Your order is confirmed';
$mail->Body = 'Hello John, your order #12345 has been received.';
$mail->send();
echo 'Email sent successfully';
} catch (Exception $e) {
echo "Error: {$mail->ErrorInfo}";
}
When a send fails, $mail->ErrorInfo gives you a descriptive error message. Always wrap PHPMailer calls in try/catch and echo that property during development so you can actually see what went wrong.
Send an HTML email with PHPMailer
To send HTML with a plain text fallback for non-HTML clients, enable isHTML(true) and set both Body (HTML) and AltBody (plain text):
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'smtp-relay.brevo.com';
$mail->SMTPAuth = true;
$mail->Username = '[email protected]';
$mail->Password = 'YOUR_SMTP_KEY';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('[email protected]', 'My Store');
$mail->addAddress('[email protected]', 'Jane Doe');
$mail->addReplyTo('[email protected]', 'Order Support');
$mail->isHTML(true);
$mail->Subject = 'Order Confirmation #98765';
$mail->Body = '<html><body>
<h1>Order confirmed!</h1>
<p>Your order <strong>#98765</strong> is on its way.</p>
<p>You can track your shipment <a href="https://example.com/track/98765">here</a>.</p>
</body></html>';
$mail->AltBody = 'Order confirmed! Your order #98765 is on its way. Track: https://example.com/track/98765';
$mail->send();
echo 'HTML email sent successfully';
} catch (Exception $e) {
echo "Mailer Error: {$mail->ErrorInfo}";
}
The AltBody text version shows up in non-HTML clients and also helps with spam filters that flag HTML-only messages.
Send to multiple recipients with PHPMailer
$mail->addAddress('[email protected]', 'Alice');
$mail->addAddress('[email protected]', 'Bob');
$mail->addCC('[email protected]', 'Manager');
$mail->addBCC('[email protected]');
$mail->addReplyTo('[email protected]', 'Support');
Call addAddress() for each person in the To field. Use addCC() and addBCC() for CC and BCC. If recipients shouldn't see each other's addresses, use BCC or send in a loop.
Send email with attachments using PHPMailer
PHPMailer makes attachments straightforward with addAttachment():
$mail->addAttachment('/path/to/invoice.pdf', 'Invoice.pdf');
$mail->addAttachment('/path/to/receipt.png', 'Receipt.png');
Combine this with the HTML setup above and you have a formatted email with files attached.
Enable debug output for testing
During development, turn on PHPMailer's SMTP debug output to troubleshoot connection issues:
$mail->SMTPDebug = 2; // 0 = off, 1 = client messages, 2 = client + server
This lets you confirm your SMTP credentials work and that Brevo's server is accepting your test messages before you move to production.
Method 3: Brevo PHP SDK v4 (REST API)
If your project needs more than SMTP can offer (delivery analytics, template-based sending, webhooks, programmatic contact management), the Brevo REST API through the official PHP SDK is the way to go. You get features like open/click tracking, bounce management, and dedicated IP pools that simply aren't available over SMTP.
Prerequisites
- Verify your sender address in Brevo under Settings > Senders & IPs > Senders. API calls will return a 400 error if you skip this.
- Generate an API key under Settings > SMTP & API > API Keys > Create a new API key. Store it in an environment variable. Never hardcode credentials in your PHP files or source code.
Install the SDK
composer require getbrevo/brevo-php
The v4 SDK requires PHP 8.1 or higher. If you're running an older version (including many WordPress environments on PHP 7.x), use the legacy v2 SDK instead, covered further down.
Send your first email
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Brevo\Client\Brevo;
use Brevo\Client\TransactionalEmails\SendTransacEmailRequest;
use Brevo\Client\TransactionalEmails\SendTransacEmailRequestSender;
use Brevo\Client\TransactionalEmails\SendTransacEmailRequestToItem;
$client = new Brevo(apiKey: $_ENV['BREVO_API_KEY']);
$request = new SendTransacEmailRequest(
subject: 'Order Confirmation',
htmlContent: '<html><body><p>Your order <strong>#{{params.orderId}}</strong> is confirmed!</p></body></html>',
params: ['orderId' => '98765'],
sender: new SendTransacEmailRequestSender(
email: '[email protected]',
name: 'Your Store'
),
to: [
new SendTransacEmailRequestToItem(
email: '[email protected]',
name: 'Jane Doe'
)
]
);
$response = $client->transactionalEmails->sendTransacEmail($request);
echo 'Message ID: ' . $response->getMessageId();
A successful call returns HTTP 201 with a messageId in the response body.
Error handling and rate limits
The v4 SDK throws a BrevoApiException for any non-2xx response. Brevo enforces a rate limit of roughly 600 emails/hour on paid plans. When you exceed it, the API responds with HTTP 429 and a Retry-After header:
<?php
use Brevo\Client\Brevo;
use Brevo\Client\Exceptions\BrevoApiException;
$client = new Brevo(apiKey: $_ENV['BREVO_API_KEY']);
try {
$response = $client->transactionalEmails->sendTransacEmail($request);
echo 'Sent! Message ID: ' . $response->getMessageId();
} catch (BrevoApiException $e) {
$code = $e->getCode();
if ($code === 429) {
echo 'Rate limit hit. Retry after: ' . $e->getMessage();
sleep(60);
} elseif ($code === 401) {
echo 'Invalid API key';
} elseif ($code === 402) {
echo 'Insufficient credits - upgrade your plan';
} else {
echo "API Error {$code}: " . $e->getBody();
}
}
For sustained high-volume sending, implement exponential backoff:
function sendWithRetry(Brevo $client, $request, int $maxRetries = 3): mixed
{
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
try {
return $client->transactionalEmails->sendTransacEmail($request);
} catch (BrevoApiException $e) {
if ($e->getCode() === 429) {
$wait = pow(2, $attempt) * 10;
echo "Rate limit hit. Waiting {$wait}s (attempt " . ($attempt + 1) . "/{$maxRetries})...\n";
sleep($wait);
} else {
throw $e;
}
}
}
throw new RuntimeException('Max retries exceeded');
}
Method 4: raw cURL (no Composer, no SDK)
On shared hosting or legacy PHP apps where Composer isn't an option, you can call the Brevo REST API directly with PHP's native cURL functions. It also works well as a quick test to verify your API key before wiring up the full SDK.
<?php
$apiKey = $_ENV['BREVO_API_KEY'];
$payload = json_encode([
'sender' => ['email' => '[email protected]', 'name' => 'My App'],
'to' => [['email' => '[email protected]', 'name' => 'User']],
'subject' => 'Welcome to My App!',
'htmlContent' => '<html><body><h1>Welcome!</h1><p>Thanks for signing up.</p></body></html>',
]);
$ch = curl_init('https://api.brevo.com/v3/smtp/email');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'api-key: ' . $apiKey,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 201) {
$data = json_decode($response, true);
echo 'Sent! Message ID: ' . $data['messageId'];
} elseif ($httpCode === 429) {
echo 'Rate limit exceeded. Try again shortly.';
} else {
echo "Error {$httpCode}: {$response}";
}
This gives you a working send-email-from-PHP solution with no external dependencies at all.
Laravel integration
Laravel's Mail facade uses its own mailer abstraction, and the cleanest way to hook up Brevo is through the SMTP relay configured in .env. No extra packages needed:
MAIL_MAILER=smtp
MAIL_HOST=smtp-relay.brevo.com
MAIL_PORT=587
MAIL_PASSWORD=YOUR_SMTP_KEY
MAIL_ENCRYPTION=tls
MAIL_FROM_NAME="My App"
This works with all standard Laravel mail features: Mail::to()->send(), queued Mailable classes, and notification channels. Same rule applies here: MAIL_PASSWORD is your Brevo SMTP key, not your API key.
If your team wants native Brevo API integration for things like templates, webhooks, or contact sync, you can use the Brevo PHP SDK directly within a Mailable or service class alongside Laravel's DI container.
Legacy v2 SDK (PHP 7.x / WordPress)
If you're running PHP below 8.1 (which includes many WordPress and older Symfony projects), the v4 SDK won't work. Use the v2 SDK instead:
composer require getbrevo/brevo-php "1.x.x"
<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = Brevo\Client\Configuration::getDefaultConfiguration()
->setApiKey('api-key', $_ENV['BREVO_API_KEY']);
$apiInstance = new Brevo\Client\Api\TransactionalEmailsApi(
new GuzzleHttp\Client(),
$config
);
$sendSmtpEmail = new Brevo\Client\Model\SendSmtpEmail([
'sender' => ['email' => '[email protected]', 'name' => 'My App'],
'to' => [['email' => '[email protected]', 'name' => 'User']],
'subject' => 'Welcome!',
'htmlContent' => '<html><body><h1>Welcome!</h1></body></html>',
]);
try {
$result = $apiInstance->sendTransacEmail($sendSmtpEmail);
echo 'Message ID: ' . $result->getMessageId();
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage();
}
Brevo maintains v2 with security updates. The underlying REST API is the same; only the PHP client structure differs.
Sending personalized emails at scale
For batch sending (order confirmations, welcome sequences, notification bursts), don't loop over individual API calls. The Brevo API supports up to 1,000 personalized email versions in a single request using the messageVersions parameter, each with its own recipient, subject, and template parameters. That cuts your rate limit exposure by up to 1,000x compared to individual sends.
For smaller lists, a loop with sendWithRetry() (shown above) works fine. Brevo's rate limit is around 600 emails/hour on paid plans; for higher throughput, use the batch endpoint. Full details are in the Brevo API reference.
Brevo's free plan gives you 300 emails/day with full API access, no credit card required, which is a practical starting point for testing your integration. Brevo's paid plans start at $9/month for 5,000 emails, billed per email sent rather than per contact stored.
Security best practices
A few rules that apply to any PHP email setup before it goes live:
- Never hardcode credentials. Use environment variables ($_ENV['BREVO_API_KEY']) or a secrets manager. Never put your email password directly in the code.
- Use TLS encryption. Always use port 587 with STARTTLS or port 465 with SSL. Never send credentials over an unencrypted connection.
- SMTP key and API key are different things. These are distinct credentials in Brevo. Mixing them up is the most common PHP authentication failure.
- SMTP password is not your account password. Your SMTP password is the key generated in Settings > SMTP & API > SMTP Keys, not your Brevo login password.
- Validate the From address. It must belong to a domain you own with SPF and DKIM configured. Emails from unverified domains will fail authentication checks and end up in spam.
- Set a Reply-To address. Add a reply-to on all outbound email so replies reach the right inbox, not an unmonitored sender address.
If you're setting up a new sending domain, warm it up gradually to build sender reputation before sending at full volume.
Quick reference: which method to use
What's next
The Brevo developer documentation covers the full API reference, the PHP SDK documentation has setup details, and you'll find webhook setup for delivery events (opens, clicks, bounces) and template-based sending there too. For a broader view of email API options, see Brevo's overview of the best email APIs and the complete email marketing API guide.







