Sending Email with Attachments in PHP: A Comprehensive Guide

Sending emails with PHP is a common task for web developers, but adding attachments introduces additional complexity, such as handling file uploads, encoding binary data, and constructing a multipart email. This guide provides a detailed walkthrough of creating an HTML form and a robust PHP script to send emails with multiple file attachments securely. Whether you’re building a contact form or an automated email system, this tutorial will equip you with the knowledge to implement this feature effectively.

Creating the HTML Form

The HTML form is the user interface for collecting input, including text fields and file uploads. To handle file attachments, the form must use the multipart/form-data encoding type, which allows binary data (like files) to be sent to the server. Below is a clean, accessible form with detailed explanations of each component.

Form Code

123456789101112131415161718192021
<form enctype="multipart/form-data" method="POST" action="send_email.php">
    <label for="sender_name">Your Name</label>
    <input type="text" id="sender_name" name="sender_name" required>
    
    <label for="sender_email">Your Email</label>
    <input type="email" id="sender_email" name="sender_email" required>
    
    <label for="subject">Subject</label>
    <input type="text" id="subject" name="subject" required>
    
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>
    
    <label for="phone">Phone</label>
    <input type="tel" id="phone" name="phone" pattern="[0-9]{10,15}" required>
    
    <label for="my_files">Attachment(s)</label>
    <input type="file" id="my_files" name="my_files[]" multiple accept=".pdf,.doc,.docx,.jpg,.png">
    
    <button type="submit" name="button">Submit</button>
</form>

Form Explanation

  • Encoding Type: The enctype="multipart/form-data" attribute is critical for file uploads, as it tells the browser to encode the form data in a way that supports binary files, unlike the default application/x-www-form-urlencoded.
  • Input Fields:
    • Name, Email, Subject: These are standard text inputs marked as required to ensure users provide them. The email type enforces basic email format validation in modern browsers.
    • Message: A <textarea> allows multi-line input for the email body, with rows="5" for better usability.
    • Phone: The tel input with a pattern="[0-9]{10,15}" ensures the phone number contains only digits and is between 10 and 15 characters long, covering most international formats.
    • File Input: The name="my_files[]" with multiple allows uploading multiple files. The accept attribute restricts uploads to common file types (e.g., PDFs, Word documents, images) to guide users.
  • Accessibility: Each <input> and <textarea> has an associated <label> with a for attribute matching the input’s id, improving accessibility for screen readers.
  • Action: The form’s action="send_email.php" points to the PHP script that processes the submission.

PHP Script for Sending Email with Attachments

The PHP script handles form submissions, validates inputs, processes file uploads, and sends an email with attachments using PHP’s mail() function. It includes robust validation, error handling, and security measures to ensure reliable operation.

PHP Code

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
&lt;?php
// Configuration
$recipient_email = "[email protected]"; // Replace with the recipient's email address
$from_email = "[email protected]"; // Replace with your domain's email address

// Enable error reporting for development (disable in production)
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Check if the request is a POST submission
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // Validate and sanitize inputs
    $sender_name = filter_input(INPUT_POST, "sender_name", FILTER_SANITIZE_STRING);
    if (empty($sender_name)) {
        exit("Error: Name is required.");
    }

    $sender_email = filter_input(INPUT_POST, "sender_email", FILTER_VALIDATE_EMAIL);
    if (!$sender_email) {
        exit("Error: Invalid email address.");
    }

    $phone_number = filter_input(INPUT_POST, "phone", FILTER_SANITIZE_NUMBER_INT);
    if (empty($phone_number) || !preg_match("/^[0-9]{10,15}$/", $phone_number)) {
        exit("Error: Phone number must be 10-15 digits.");
    }

    $subject = filter_input(INPUT_POST, "subject", FILTER_SANITIZE_STRING);
    if (empty($subject)) {
        exit("Error: Subject is required.");
    }

    $message = filter_input(INPUT_POST, "message", FILTER_SANITIZE_STRING);
    if (empty($message)) {
        exit("Error: Message is required.");
    }

    // Construct the message body
    $message_body = "Message from: $sender_name\n";
    $message_body .= "Email: $sender_email\n";
    $message_body .= "Phone: $phone_number\n";
    $message_body .= "------------------------------\n";
    $message_body .= "$message\n";
    $message_body .= "------------------------------\n";

    // Generate a unique boundary for multipart email
    $boundary = md5(uniqid(time()));

    // Set up email headers
    $headers = "MIME-Version: 1.0\r\n";
    $headers .= "From: $from_email\r\n";
    $headers .= "Reply-To: $sender_email\r\n";

    // Initialize email body
    $body = "";

    // Handle file attachments
    $attachments = $_FILES['my_files'] ?? [];
    $file_count = count($attachments['name'] ?? []);

    if ($file_count &gt; 0) {
        // Set multipart/mixed content type for attachments
        $headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";

        // Add message part
        $body .= "--$boundary\r\n";
        $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $body .= chunk_split(base64_encode($message_body));

        // Process each attachment
        for ($i = 0; $i &lt; $file_count; $i++) {
            if ($attachments['error'][$i] === UPLOAD_ERR_OK &amp;&amp; !empty($attachments['name'][$i])) {
                $file_name = $attachments['name'][$i];
                $file_size = $attachments['size'][$i];
                $file_type = $attachments['type'][$i];
                $file_tmp = $attachments['tmp_name'][$i];

                // Validate file size (max 5MB)
                if ($file_size &gt; 5 * 1024 * 1024) {
                    exit("Error: File '$file_name' exceeds 5MB limit.");
                }

                // Validate file type (optional, based on allowed types)
                $allowed_types = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/png'];
                if (!in_array($file_type, $allowed_types)) {
                    exit("Error: File '$file_name' has an unsupported type.");
                }

                // Read file content
                $content = file_get_contents($file_tmp);
                if ($content === false) {
                    exit("Error: Failed to read file '$file_name'.");
                }

                // Encode file content
                $encoded_content = chunk_split(base64_encode($content));

                // Add attachment part
                $body .= "--$boundary\r\n";
                $body .= "Content-Type: $file_type; name=\"$file_name\"\r\n";
                $body .= "Content-Disposition: attachment; filename=\"$file_name\"\r\n";
                $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
                $body .= "$encoded_content\r\n";
            } elseif ($attachments['error'][$i] !== UPLOAD_ERR_NO_FILE) {
                $errors = [
                    UPLOAD_ERR_INI_SIZE =&gt; "File '$file_name' exceeds the upload_max_filesize directive in php.ini.",
                    UPLOAD_ERR_FORM_SIZE =&gt; "File '$file_name' exceeds the MAX_FILE_SIZE directive in the HTML form.",
                    UPLOAD_ERR_PARTIAL =&gt; "File '$file_name' was only partially uploaded.",
                    UPLOAD_ERR_NO_FILE =&gt; "No file was uploaded.",
                    UPLOAD_ERR_NO_TMP_DIR =&gt; "Missing a temporary folder.",
                    UPLOAD_ERR_CANT_WRITE =&gt; "Failed to write file to disk.",
                    UPLOAD_ERR_EXTENSION =&gt; "A PHP extension stopped the file upload."
                ];
                exit($errors[$attachments['error'][$i]] ?? "Unknown upload error for '$file_name'.");
            }
        }
        $body .= "--$boundary--\r\n";
    } else {
        // Plain text email without attachments
        $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
        $body = $message_body;
    }

    // Send the email
    $sent = mail($recipient_email, $subject, $body, $headers);
    echo $sent ? "Thank you! Your email has been sent successfully." : "Error: Could not send email. Please check your PHP mail configuration.";
    exit;
}
?&gt;

PHP Code Explanation

  • Configuration:
    • $recipient_email and $from_email define the recipient and sender email addresses. Replace these with your actual email addresses.
    • Error reporting is enabled for development to catch issues early. Disable display_errors in production for security.
  • Input Validation:
    • Uses filter_input with FILTER_SANITIZE_STRING and FILTER_VALIDATE_EMAIL to sanitize and validate inputs, preventing injection attacks.
    • The phone number is validated with a regex (/^[0-9]{10,15}$/) to ensure it contains only digits and meets length requirements.
    • Empty or invalid inputs trigger specific error messages and halt execution.
  • Message Body:
    • Constructs a plain text message including the sender’s name, email, phone number, and message, separated by clear dividers for readability.
  • File Handling:
    • Checks for uploaded files via $_FILES['my_files'].
    • Validates file size (max 5MB) and type (PDF, Word, JPEG, PNG) to ensure compatibility and security.
    • Uses file_get_contents for reliable file reading, replacing the less secure fopen/fread.
    • Encodes file content in base64 and splits it into chunks per RFC 2045 for email compatibility.
  • Email Structure:
    • Uses a unique boundary (generated with md5(uniqid(time()))) to separate message parts in a multipart email.
    • Sets MIME version, From, and Reply-To headers. For attachments, uses multipart/mixed; otherwise, uses text/plain with UTF-8 encoding.
    • The message body is base64-encoded for compatibility with various email clients.
  • Attachment Processing:
    • Iterates through uploaded files, skipping those with errors (except UPLOAD_ERR_NO_FILE for optional uploads).
    • Adds each valid file as a separate part in the email body with appropriate headers (Content-Type, Content-Disposition, Content-Transfer-Encoding).
    • Closes the multipart email with a final boundary (--boundary--).
  • Error Handling:
    • Detailed upload error messages are mapped to PHP’s UPLOAD_ERR_* constants, providing user-friendly feedback.
    • File size and type validation prevent server overload or security risks.
  • Email Sending:
    • The mail() function sends the email. Success or failure is reported to the user.
    • In production, consider using a library like PHPMailer for better reliability and features (e.g., SMTP support).

Key Improvements Over the Original

  • HTML Form:
    • Added id attributes and for labels for accessibility.
    • Included required attributes and a pattern for phone validation.
    • Added an accept attribute to guide users on allowed file types.
    • Used a <button> instead of <input> for the submit button for better styling flexibility.
  • PHP Code:
    • Replaced $_POST with filter_input for secure input handling.
    • Added file type validation to restrict uploads to safe formats.
    • Implemented a 5MB file size limit to prevent server issues.
    • Used file_get_contents for simpler, more reliable file reading.
    • Added UTF-8 encoding for better international support.
    • Improved error messages with specific feedback for each validation failure.
    • Structured the code with clear comments and modern PHP practices (e.g., strict comparison, early exits).
  • Security:
    • Sanitized all inputs to prevent injection attacks.
    • Validated file types and sizes to mitigate security risks.
    • Used a unique boundary for each email to avoid conflicts.

How to Use

  1. Set Up the Form:
    • Save the HTML form in a file (e.g., index.html) or integrate it into your website.
    • Ensure the form’s action attribute points to send_email.php.
  2. Configure the PHP Script:
    • Save the PHP code as send_email.php in your server’s root or appropriate directory.
    • Update $recipient_email and $from_email with your actual email addresses.
    • Verify that your server has PHP’s mail() function enabled or configured with an SMTP server.
  3. Test the Form:
    • Upload the files to your server and access the form via a browser.
    • Test with and without attachments to ensure both plain and multipart emails work.
    • Check your server’s error logs if the email fails to send.
  4. Production Considerations:
    • Disable display_errors in production to avoid exposing sensitive information.
    • Consider using a library like PHPMailer or SwiftMailer for robust email delivery, especially for high-volume or SMTP-based sending.
    • Adjust the upload_max_filesize and post_max_size in php.ini if you need to allow larger files.
  5. Optional Enhancements:
    • Add client-side validation with JavaScript for instant feedback.
    • Implement an Ajax-based submission to avoid page reloads (see resources for Ajax tutorials).
    • Add a CAPTCHA to prevent spam submissions.

Troubleshooting Tips

  • Email Not Sending: Ensure your server’s mail() function is configured correctly. Check the server’s mail log (e.g., /var/log/maillog) for errors. Alternatively, switch to PHPMailer with SMTP settings.
  • File Upload Issues: Verify that upload_max_filesize and post_max_size in php.ini are sufficient. Ensure the temporary upload directory is writable.
  • Invalid File Types: Adjust the $allowed_types array in the PHP code to include other file types as needed.
  • Email Formatting Issues: Test with multiple email clients (e.g., Gmail, Outlook) to ensure attachments and message body display correctly.

Conclusion

This PHP email script provides a reliable, secure way to send emails with attachments from a web form. By following best practices for input validation, file handling, and email construction, you can integrate this solution into your projects with confidence. For advanced features, such as asynchronous form submission, explore Ajax-based implementations. Experiment with this code, and let us know your results in the comments!

Call to Action: Have you used PHP’s mail() function for attachments before? Share your tips or questions below!