Creating an HTML e-mail form that allows for file attachments – part 3

This is the final part of the series on creating the form. It cleans up some loose ends and adds some bells & whistles to make the form more flexible.

One thing I wanted to add was the ability to “cc” the submitter. This is fairly easy. I start by adding a flag in the form a checkbox form field. You could also set it to “unchecked” or even “”. The form can override this through a hidden or visible field:

<nput type="checkbox" name="_cc_sender" id="_cc_sender" value="checked" hidden />

In the FormMail.php script, we can test for this very easily. If they want the submitter to be cc’d, we just need to add them to the recipients as such:

    if (isset($_POST['_cc_sender'])) {
        $send_to = $from_email . "," . $to;
    } else {
        $send_to = $to;
    }

Then when we go to send the e-mail, use $send_to instead of $to.

    $mail_obj->send($send_to, $headers, $body);

Because we are using sendmail, so long as the addresses in $send_to aren’t in $headers, they won’t show in the e-mail.

The big change is that I wanted the form to be able to handle sending multiple large files as attachments. While the previous version could do that in theory, in practice it could neither send e-mails with too many large attachments nor could I receive them.

Most e-mail systems don’t let you send or receive large e-mails. The exact limit vary widely but they usually exist. And as e-mail gets routed through various hosts, it could conceivably be rejected as too large by any one of them.

Now to be clear, this version doesn’t let you send huge single files. However, if you are attaching multiple files, each of which could pass through your e-mail provider’s size restrictions, this will break up the form mail into multiple parts. Each part will contain one or more of the attachments.

The first thing we need to do is set a breakpoint. If an attachment makes the e-mail larger than the breakpoint, it will be sent. Otherwise, it will continue to accumulate attachments. This allows for attachments that are larger than the breakpoint – they will be added and then sent – which means the breakpoint has to be fairly small.

In my particular use case, I found that that I needed to set it at no more than about 2M or the script would simply fail. I was using more space than my host would allow.

Since we’re going to be dealing primarily in terms of megabytes, let’s make our numbers easy to use by defining a few values:

// Global Constants
$_megaByte      = 1024 * 1024; 
$_split_size    =  2 * $_megaByte;
$_max_upload    = 20 * $_megaByte;

We’ll also need to keep track of the size and number of parts. I also keep a separate track of whether we need to send the current message body. If we knew that there would always be attachments, this could simply be

$_cur_size      = 0;    // total size of files attached to current part-message
$_cur_part      = 1;    // which sub-message are we working on now
$_tot_size      = 0;    // what is the total size of all attachments added to 

I’ve found that even when I split the attachments across multiple e-mails, at some point the script would not send them all. Sometimes it would simply never complete while at other times it would appear to work but I’d either never receive anything or receive an e-mail without attachments. To handle that, I force an error message if the submitter exceeds the $_max_upload limit.

    if ($_tot_size > $_max_upload) {
        reportError("Your upload (" . strval(intval($_tot_size / $_megaByte)) . "MB) is too large - the maximum total size allowed is " . strval($_max_upload / $_megaByte) . "MB.");
        reportError("Your submission might not have gone through.");
        if (isset($_POST['_cc_sender'])) {
            reportError("Check your e-mail to confirm if all the files were sent.");
        }
    }

So how do we actually split up the e-mail? After each file is attached, we check to see if the $_split_size has been exceeded. If it has, we send the e-mail with the attachment(s) then reset everything for the next e-mail. To make this easier, let’s split out the actual sending of the e-mail like this:

function sendEmail(&$message, $subject, $to, $from_email) {
    if (empty($_errors)) {
        $body = $message->get();
        if (isset($_POST['_cc_sender'])) {
            $send_to = $from_email . "," . $to;
        } else {
            $send_to = $to;
        }
        $extraheaders = array("From"=>$from_email, "Subject"=>$subject);
        $headers  = $message->headers($extraheaders);
        $mail_obj = Mail::factory("sendmail");
        $mail_obj->send($send_to, $headers, $body);
    }
}

Let’s do the same for adding each attachment:

function attachFile($name, $tmp_path, &$message) {
    global $_subject, $_cur_size, $_split_size, $_cur_part, $_to, $_from_email, $_text;
    if (strlen($name) > 0) {
        $_cur_size += copyAndAddAttachment($tmp_path, $name, $message);
        if ($_cur_size > $_split_size) {
            sendEmail($message, $_subject . ": part " . strval($_cur_part), $_to, $_from_email);
            $message = new Mail_mime(); 
            $message->setTXTBody($_text);
            $_cur_size = 0;
            $_cur_part += 1;
        }
    }
}

By separating out the sending of the e-mail, we can use it here and also to handle the cases where there is no attachment (or where the $_split_size hadn’t been reached). We need to split out attaching the files because it now has to handle sending e-mail and resetting counters. This simplifies the main loop for adding attachments:

   foreach ($_FILES as &$userfile) {
        if (is_array($userfile['name'])) {
            foreach ($userfile['name'] as $currentfile=>$name) {
                $tmp_path = $userfile["tmp_name"][$currentfile];
                attachFile($name, $tmp_path, $_message);
            }
        } else {
            $name     = $userfile['name'];
            $tmp_path = $userfile["tmp_name"];
            attachFile($name, $tmp_path, $_message);
        }
    }

Now we are just left to clean up at the end. If the submitter didn’t attach any files, no e-mail would have been sent yet. And if the last attachment was below the $_split_size, it may also not have been sent.

To handle this, I test if the $_cur_part is 1. If it is, we still need to send the e-mail regardless of whether there is an attachment or not. No need to add a part # however, since there is only the one e-mail.

However, if $_cur_part is not 1, there may or may be an unsent attachment. $_cur_size tells us if we need to send another message part or not. If it’s zero in size, no need to send it. This could be incorrect in the case where the last attachment is a zero-length file. If you are concerned about that degenerate case, you can always set a “this needs to be sent” flag after attaching each file and resetting it after each part is sent.

    if (empty($_errors)) {
        if ($_cur_part == 1) {  // we haven't sent anything yet
            sendEmail($_message, $_subject, $_to, $_from_email);
        } else {
            if ($_cur_size > 0) {  // there is an unsent attachment
                sendEmail($_message, $subject . ": part " . strval($_cur_part), $to, $from_email);
            }
        }        
    }
  

Below is the final code:

<?php 
// Pear library includes
// You should have the pear lib installed
include_once('Mail.php');
include_once('Mail/mime.php');

// Set these in your form page
// _recipient  -- address to send e-mail to
// _subject    -- subject line for e-mail
// _redirect   -- go to this page after e-mail is successfully
// _cc_sender  -- optionally mark this as checked if you want to cc the person submitting the form
//                you can also have the user set it in the form if you want them to decide

// required fields -- these can be set by the person filling in the form
// realname    -- the sender's name
// email       -- the sender's e-mail address

// Global Constants
$_megaByte      = 1024 * 1024;

// Global Settings 
$upload_folder  = './uploads/'; //<-- this folder must be writeable by the script
$_split_size    =  2 * $_megaByte;
$_max_upload    = 20 * $_megaByte;

// Global variables
$_errors        = '';
$_cur_size      = 0;    // total size of files attached to current part-message
$_cur_part      = 1;    // which sub-message are we working on now
$_tot_size      = 0;    // what is the total size of all attachments added to all message parts so far

function reportError($message) {
    global $_errors;
    $_errors .= $message . "<br />";
}

function copyAndAddAttachment($from_path, $to_path, &$message) {
    global $_cur_size, $_tot_size, $_needToSend;
    $to_file = $upload_folder . $to_path;
    if (move_uploaded_file($from_path, $to_file)) {
        $_fileSize = filesize($to_file);
        $_tot_size += $_fileSize;
        $message->addAttachment($to_file);
        unlink($to_file);
        return $_fileSize;
    } else {
        unlink($from_path);  // remove file that failed to copy - otherwise it clogs up the site
        reportError("error moving " . $from_path . " to " . $to_file);
    }
}

function sendEmail(&$message, $subject, $to, $from_email) {
    if (empty($_errors)) {
        $body = $message->get();
        if (isset($_POST['_cc_sender'])) {
            $send_to = $from_email . "," . $to;
        } else {
            $send_to = $to;
        }
        $extraheaders = array("From"=>$from_email, "Subject"=>$subject);
        $headers  = $message->headers($extraheaders);
        $mail_obj = Mail::factory("sendmail");
        $mail_obj->send($send_to, $headers, $body);
    }
}

function attachFile($name, $tmp_path, &$message) {
    global $_subject, $_cur_size, $_split_size, $_cur_part, $_to, $_from_email, $_text;
    if (strlen($name) > 0) {
        $_cur_size += copyAndAddAttachment($tmp_path, $name, $message);
        if ($_cur_size > $_split_size) {
            sendEmail($message, $_subject . ": part " . strval($_cur_part), $_to, $_from_email);
            $message = new Mail_mime(); 
            $message->setTXTBody($_text);
            $_cur_size = 0;
            $_cur_part += 1;
        }
    }
}

if (!isset($_POST['submit'])) {
    reportError("post submit not found");
} else {
    $_name       = $_POST['realname'];
    $_from_email = $_POST['email'];
    $_to         = $_POST['_recipient'];
    $_subject    = $_POST['_subject'];
    $_text       = $_name . " has sent you this information:\n";
    foreach ($_POST as $input_field=>$value) {
        if (substr($input_field, 0, 1) != "_") {    // skip preset values
            if (!empty($value)) {                   // skip blank fields
                $_text .= $input_field . ": " . $value . "\n";
            }
        }
    }
    $_message = new Mail_mime(); 
    $_message->setTXTBody($_text); 
// attachments may be optional. Only add one if a file is uploaded.
// note that you cannot check isset($_FILES) because that will be true even if no file is selected
    foreach ($_FILES as &$userfile) {
        if (is_array($userfile['name'])) {
            foreach ($userfile['name'] as $currentfile=>$name) {
                $tmp_path = $userfile["tmp_name"][$currentfile];
                attachFile($name, $tmp_path, $_message);
            }
        } else {
            $name     = $userfile['name'];
            $tmp_path = $userfile["tmp_name"];
            attachFile($name, $tmp_path, $_message);
        }
    }
    
// send the (last part of the) email if no errors
    if (empty($_errors)) {
        if ($_cur_part == 1) {  // we haven't sent anything yet
            sendEmail($_message, $_subject, $_to, $_from_email);
        } else {
            if ($_cur_size > 0) {  // there is an unsent attachment
                sendEmail($_message, $subject . ": part " . strval($_cur_part), $to, $from_email);
            }
        }        
    }
    if ($_tot_size > $_max_upload) {
        reportError("Your upload (" . strval(intval($_tot_size / $_megaByte)) . "MB) is too large - the maximum total size allowed is " . strval($_max_upload / $_megaByte) . "MB.");
        reportError("Your submission might not have gone through.");
        if (isset($_POST['_cc_sender'])) {
            reportError("Check your e-mail to confirm if all the files were sent.");
        }
    }
}

if (empty($_errors)) {
// redirect to page confirming e-mail was sent
    $success_page = $_POST['_redirect'];
    header("Location: $success_page");
} else {
    header('Location: error.php?errors=' . urlencode($_errors));
}

?>

About Gary Dale

Gary Dale is a long time social justice activist who has served in a number of roles. He is best known for founding and running FaxLeft in the 1990s, for running in Ontario and Canada elections, and for serving on the National Council of Fair Vote Canada. He has had a large number of letters to the editor published in a variety of media and on a wide range of topics.
This entry was posted in Computers, Education, Internet, Science and Mathematics. Bookmark the permalink.

Leave a comment