CloudPloy

Email and SMTP Configuration for PHP Applications

PHP's built-in mail() function sends email directly from the server's local MTA (Postfix or Sendmail). In cloud environments this almost never works reliably: cloud servers have no mail server installed, and even if they did, email from unknown IP addresses is blocked or junked by Gmail, Outlook, and most corporate mail filters. The right approach for production PHP applications is to route all outbound email through a dedicated SMTP or API-based email service.

This page covers configuring email delivery for Laravel, WordPress, and plain PHP applications on CloudPloy, choosing an email provider, setting up DNS records for deliverability, and handling high email volumes with queues.

Choosing an Email Service Provider

Email service providers (ESPs) handle the infrastructure for reliable email delivery: dedicated IP addresses with established reputations, bounce and complaint handling, DNS authentication, and delivery analytics. For transactional email (order confirmations, password resets, notifications) these are the main options:

Provider Free Tier Best For PHP/Laravel Support
Mailgun 100 emails/day Developers; excellent API and SMTP Official Laravel driver; Symfony Mailer transport
Postmark 100 emails/month (testing) Transactional email; best deliverability track record Official Laravel driver; Symfony Mailer transport
Resend 3,000 emails/month Developer-friendly; modern API Laravel driver via community package
Amazon SES 62,000 emails/month from EC2 High volume at very low cost Official Laravel driver (SES driver)
SendGrid 100 emails/day Transactional + marketing; large teams SMTP or API via community package
Brevo (ex-Sendinblue) 300 emails/day Small businesses; includes marketing email SMTP

For most CloudPloy applications under 10,000 emails/month, Mailgun or Resend offer the best developer experience. For high volume, Amazon SES is the most cost-effective at $0.10 per 1,000 emails.

Laravel Email Configuration

Environment Variables

Configure your email provider in App > Settings > Environment Variables. Never hardcode credentials in config/mail.php.

For SMTP (works with any provider):

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org       # Your provider's SMTP host
MAIL_PORT=587
MAIL_USERNAME=your-smtp-username
MAIL_PASSWORD=your-smtp-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@yourapp.com
MAIL_FROM_NAME="Your App Name"

For Mailgun via API (faster than SMTP, no port 587 needed):

MAIL_MAILER=mailgun
MAILGUN_DOMAIN=mg.yourapp.com
MAILGUN_SECRET=key-your-mailgun-api-key
MAILGUN_ENDPOINT=api.mailgun.net  # or api.eu.mailgun.net for EU region

For Amazon SES:

MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1

For Postmark:

MAIL_MAILER=postmark
POSTMARK_TOKEN=your-server-api-token

Sending Email in Laravel

Laravel's Mail facade sends via whatever driver is configured in MAIL_MAILER. Create a Mailable class:

// app/Mail/OrderConfirmation.php
class OrderConfirmation extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "Order #{$this->order->id} Confirmed",
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.order-confirmation',
            with: ['order' => $this->order],
        );
    }
}

Send immediately:

Mail::to($order->customer_email)->send(new OrderConfirmation($order));

Or queue it (recommended for production - does not block the HTTP response):

Mail::to($order->customer_email)->queue(new OrderConfirmation($order));

Queue-Based Email in Production

Sending email synchronously during a web request adds 100-500ms to response time. For any site sending more than a few emails per day, queue all outbound email:

  1. Set QUEUE_CONNECTION=redis in environment variables
  2. Use Mail::queue() instead of Mail::send() everywhere
  3. Add a queue worker in App > Workers: php artisan queue:work redis --queue=emails --tries=3

Failed email jobs are stored in the failed_jobs table. Review and retry them:

# In App > Terminal
php artisan queue:failed          # List failed jobs
php artisan queue:retry all       # Retry all failed jobs
php artisan queue:retry 5         # Retry job with ID 5

Testing Email Locally

In development, use Mailtrap or Laravel's built-in log driver to avoid sending real emails:

# .env for local development
MAIL_MAILER=log          # Writes emails to storage/logs/laravel.log

# Or use Mailtrap to preview emails in a web UI
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your-mailtrap-user
MAIL_PASSWORD=your-mailtrap-password

WordPress Email Configuration

WordPress uses PHP's mail() function by default via the wp_mail() wrapper. This will not work reliably on CloudPloy. Install WP Mail SMTP to route email through your chosen provider:

# In App > Terminal
wp plugin install wp-mail-smtp --activate

Then configure via wp-config.php to avoid storing credentials in the database (which would be exported in database backups):

define('WPMS_ON', true);
define('WPMS_MAILER', 'smtp');         // or 'mailgun', 'sendgrid', 'sendinblue'
define('WPMS_SMTP_HOST', getenv('MAIL_HOST'));
define('WPMS_SMTP_PORT', 587);
define('WPMS_SSL', 'tls');
define('WPMS_SMTP_AUTH', true);
define('WPMS_SMTP_USER', getenv('MAIL_USERNAME'));
define('WPMS_SMTP_PASS', getenv('MAIL_PASSWORD'));
define('WPMS_MAIL_FROM', getenv('MAIL_FROM_ADDRESS'));
define('WPMS_MAIL_FROM_NAME', getenv('MAIL_FROM_NAME'));

For Mailgun specifically:

define('WPMS_ON', true);
define('WPMS_MAILER', 'mailgun');
define('WPMS_MAILGUN_API_KEY', getenv('MAILGUN_SECRET'));
define('WPMS_MAILGUN_DOMAIN', getenv('MAILGUN_DOMAIN'));
define('WPMS_MAILGUN_REGION', 'US');  // or 'EU'

Test the configuration from the WP Mail SMTP plugin's "Email Test" tab in wp-admin, or from the terminal:

wp eval 'wp_mail("test@example.com", "Test", "Hello from WordPress"); echo "Sent\n";'

SPF, DKIM, and DMARC DNS Records

Email authentication records tell receiving mail servers that your ESP is authorized to send email on behalf of your domain. Without these records, email from your domain is more likely to be filtered as spam or rejected entirely.

SPF (Sender Policy Framework)

SPF lists which mail servers are allowed to send email from your domain. Add a TXT record to your domain's DNS:

# DNS TXT record for yourapp.com
Name: @ (root domain)
Type: TXT
Value: "v=spf1 include:mailgun.org ~all"

# For multiple providers:
Value: "v=spf1 include:mailgun.org include:amazonses.com ~all"

Each email provider's documentation lists their SPF include string. Use ~all (softfail) not -all (hardfail) until you're confident all your sending sources are listed, to avoid legitimate email being rejected.

DKIM (DomainKeys Identified Mail)

DKIM adds a cryptographic signature to every outbound email. The receiving server verifies the signature using a public key published in your DNS. Your email provider generates the key pair and gives you the DNS record to add.

For Mailgun, after verifying your domain in the Mailgun dashboard:

# DNS CNAME record (Mailgun example)
Name: mailo._domainkey.yourapp.com
Type: CNAME
Value: mailo.domainkey.mg.yourapp.com.mailgun.org.

Other providers use TXT records instead of CNAME. Follow the specific instructions in your provider's domain verification flow.

DMARC (Domain-based Message Authentication)

DMARC tells receiving mail servers what to do when email fails SPF or DKIM checks. Start with a monitoring-only policy, then tighten it once you've verified all your sending sources are authenticated:

# Phase 1: Monitor only (no email rejected)
Name: _dmarc.yourapp.com
Type: TXT
Value: "v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com"

# Phase 2: Quarantine suspicious email (sends to spam folder)
Value: "v=DMARC1; p=quarantine; pct=10; rua=mailto:dmarc-reports@yourapp.com"

# Phase 3: Reject unauthenticated email
Value: "v=DMARC1; p=reject; rua=mailto:dmarc-reports@yourapp.com"

The rua address receives aggregate DMARC reports showing which servers sent email from your domain and whether it passed authentication. Use a free DMARC report analyzer (Postmark's free analyzer, dmarcian, or similar) to read these XML reports.

Verify Your DNS Configuration

# Check SPF record
dig TXT yourapp.com | grep spf

# Check DKIM (replace 'mailo._domainkey' with your provider's selector)
dig TXT mailo._domainkey.yourapp.com

# Check DMARC
dig TXT _dmarc.yourapp.com

# Or use the MX Toolbox online checker for a visual report

Common Email Problems and Fixes

Problem Cause Fix
Email not sending at all MAIL_MAILER=array or log set in production Set MAIL_MAILER=smtp (or provider-specific driver) in CloudPloy env vars
Connection refused on port 587 CloudPloy server firewall blocks outbound SMTP Use the email API driver instead of SMTP, or contact support to open port 587
Email going to spam Missing SPF/DKIM records, or sending from shared IP Add DNS authentication records; use a reputable ESP with dedicated IPs
WordPress emails not sending WP Mail SMTP not installed, PHP mail() failing Install WP Mail SMTP and configure your ESP credentials
Slow page loads when sending email Email sent synchronously in web request Use Laravel queues or WP background processing to send email asynchronously
Queue worker not processing emails Worker not configured, QUEUE_CONNECTION not redis Add worker in App > Workers; set QUEUE_CONNECTION=redis
Emails with attachments failing Attachment file not accessible inside container Ensure files are in the persistent volume mount (storage/app for Laravel)
From address shows server hostname MAIL_FROM_ADDRESS not set Set MAIL_FROM_ADDRESS and MAIL_FROM_NAME in environment variables

Email Volume and Rate Limits

Most email providers have per-second or per-minute sending rate limits to protect their infrastructure. If you send bulk email (newsletters, batch notifications) without rate limiting, the provider will queue or reject the excess and you may hit daily limits.

For Laravel batch email with rate limiting:

// config/queue.php - add rate limiting for email queue
// Or use Laravel's built-in job throttling:

// In the Mailable or Job class
public function middleware(): array
{
    return [(new RateLimited('emails'))->dontRelease()];
}

// In AppServiceProvider::boot()
RateLimiter::for('emails', function (object $job) {
    return Limit::perSecond(14); // 14 emails per second = ~50,000/hour
});

For newsletters and marketing campaigns (not transactional email), use a dedicated marketing email service (Mailchimp, Campaign Monitor, Brevo) rather than routing through your transactional ESP. Marketing email has different deliverability requirements and keeping them separate protects your transactional email deliverability.


Questions about email configuration? Check App > Logs for SMTP connection errors and queue processing output, or contact CloudPloy support.