Recieve E-mail and Save Attachments with a PHP script

EDIT 4/14/2012 — There’s a new version of this script available which uses PEAR’s mimeDecode library to decode the email instead of the cobbled mess you see on this page. Go there now instead of wasting your time on this outdated page!

Here’s something fun! If you can tell your server to send e-mail to a script, you can send e-mails to PHP. Once you are processing the e-mail with PHP you can save attachments, automatically respond to the e-mail, save it to a database, make a webpage from it…really whatever you want!

Here’s a script I am currently using. My brother is on a mission for our Church, in Peru for two years and has near weekly e-mail access but can’t do much more than e-mail. He wanted a way to easily send photos to the server via e-mail; this script is the results of his wishes.

E-mail Processing Script Features:

  1. Saves the e-mail sender, subject and body to a database
  2. Saves any attachments as files and creates an entry for those files in the database, associated with the e-mail info in #1
  3. Sends  a response back to the sender telling them what files were received and their file sizes
  4. Checks a list of allowed senders to make sure we only take files from specified addresses.

Database Setup:

If you’re going to use the database features, you’ll need a database. Here’s the SQL to create an identical setup to the one I have:

-- Here's my DB structure

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
-- Table structure for table `emails`

CREATE TABLE IF NOT EXISTS `emails` (
  `id` int(100) NOT NULL AUTO_INCREMENT,
  `from` varchar(250) NOT NULL,
  `subject` text NOT NULL,
  `body` text NOT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

-- --------------------------------------------------------
-- Table structure for table `files`

CREATE TABLE IF NOT EXISTS `files` (
  `id` int(100) NOT NULL AUTO_INCREMENT,
  `email_id` int(100) NOT NULL,
  `filename` varchar(255) NOT NULL,
  `size` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

Pretty straightforward I would say. The size field in the files table stores a user friendly “100Mb” type description of the size. You will need to know your database name, username, password and host name in the next step.

The Email Handling Script:

The max_time_limit variable is how long you want the script to be allowed to run. The default max for your server might be too small to handle 20Mb of attachments (the max you can send with Google).

#!/usr/bin/php -q
<?php
//  Use -q so that php doesn't print out the HTTP headers
//  Anything printed to STDOUT will be sent back to the sender as an error!

//  Config options

$max_time_limit = 600; // in seconds
// A safe place for files with trailing slash (malicious users could upload a php or executable file!)
$save_directory = "/some/folder/path";
$allowedSenders = Array('myemail@gmail.com',
    'Bob the Builder <whatever@whoever.com>'); // only people you trust!

$send_email = TRUE; // Send confirmation e-mail?
$save_msg_to_db = TRUE; // Save e-mail body to DB?

$db_host = 'localhost';
$db_un = 'db_un';
$db_pass = 'password';
$db_name = 'db_name';

// ------------------------------------------------------

set_time_limit($max_time_limit);
ini_set('max_execution_time',$max_time_limit);

global $from, $subject, $boundary, $message, $save_path,$files_uploaded;
$save_path = $save_directory;
$files_uploaded = Array();

function formatBytes(&$bytes, $precision = 2) {
    $units = array('B', 'KB', 'MB', 'GB', 'TB');

    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);

    $bytes /= pow(1024, $pow);

    return round($bytes, $precision) . ' ' . $units[$pow];
} 

function process_part(&$email_part){
    global $message;

    // Max two parts. The data could have more than one \n\n in it somewhere,
    // but the first \n\n should be after the content info block
    $parts = explode("\n\n",$email_part,2);

    $info = split("\n",$parts[0]);
    $type;
    $name;
    $encoding;
    foreach($info as $line){
	if(preg_match("/Content-Type: (.*);/",$line,$matches)){
	    $type = $matches[1];
	}
	if(preg_match("/Content-Disposition: attachment; filename=\"(.*)\"/",
	    $line,$matches)){
	    $name = time() . "_" . $matches[1];
	}
	if(preg_match("/Content-Transfer-Encoding: (.*)/",$line,$matches)){
	    $encoding = $matches[1];
	}
    }

    // We don't know what it is, so we don't know how to process it
    if(!isset($type)){ return FALSE; }

    switch($type){
    case 'text/plain':
	// "But if you get a text attachment, you're going to overwrite
	// the real message!" Yes. I don't care in this case...
	$message = $parts[1];
	break;
    case 'multipart/alternative':
	// Multipart comes where the client sends the data in two formats so
	// that recipients who can't read (or don't like) fancy content
	// have another way to read it. Eg. When sending an html formatted
	// message, they will also send a plain text message
	process_multipart($info,$parts[1]);
	break;
    default:
	if(isset($name)){ // the main message will not have a file name...
	    // text/html messages won't be saved!
	    process_data($name,$encoding,$parts[1]);
	}elseif(!isset($message) && strpos($type,'text') !== FALSE){
	    $message = $parts[1]; // Going out on a limb here...capture
	    // the message
	}
	break;
    }
}

function process_multipart(&$info,&$data){
    global $message;

    $bounds;
    foreach($info as $line){
	if (preg_match("/boundary=(.*)$/",$line,$matches)){
	    $bounds = $matches[1];
	}
    }

    $multi_parts = split("--" .$bounds,$data);
    for($i = 1;$i < count($multi_parts);$i++){
	process_part($multi_parts[$i]);
    }
}

function process_data(&$name,&$encoding = 'base64' ,&$data){
    global $save_path,$files_uploaded;

    // find a filename that's not in use. There's a race condition
    // here which should be handled with flock or something instead
    // of just checking for a free filename

    $unlocked_and_unique = FALSE;
    while(!$unlocked_and_unique){
	// Find unique
	$name = time() . "_" . $name;
	while(file_exists($save_path . $name)) {
	    $name = time() . "_" . $name;
	}

	// Attempt to lock
	$outfile = fopen($save_path.$name,'w');
	if(flock($outfile,LOCK_EX)){
	    $unlocked_and_unique = TRUE;
	}else{
	    flock($outfile,LOCK_UN);
	    fclose($outfile);
	}
    }

    if($encoding == 'base64'){
	fwrite($outfile,base64_decode($data));
    }elseif($encoding == 'uuencode'){
	// I haven't actually seen this in an e-mail, but older clients may
	// still use it...not 100% sure that this will work correctly as is
	fwrite($outfile,convert_uudecode($data));
    }
    flock($outfile,LOCK_UN);
    fclose($outfile);

    // This is for readability for the return e-mail and in the DB
    $files_uploaded[$name] = formatBytes(filesize($save_path.$name));
}

// Process the e-mail from stdin
$fd = fopen('php://stdin','r');
$email = '';
while(!feof($fd)){ $email .= fread($fd,1024); }

// Headers hsould go till the first \n\n. Grab everything before that, then
// split on \n and process each line
$headers = split("\n",array_shift(explode("\n\n",$email,2)));
foreach($headers as $line){
    // The only 3 headers we care about...
    if (preg_match("/^Subject: (.*)/", $line, $matches)) {
	$subject = $matches[1];
    }
    if (preg_match("/^From: (.*)/", $line, $matches)) {
	$from = $matches[1];
    }
    if (preg_match("/boundary=(.*)$/",$line,$matches)){
	$boundary = $matches[1];
    }
}

// Check $from here and exit if it's blank or
// not someone you want to get mail from!
if(!in_array($from,$allowedSenders)){
    die("Not an allowed sender");
}

// No boundary was in the e-mail sent to us. We don't know what to do!
if(!isset($boundary)){
    die("I couldn't find an e-mail boundary. Maybe this isn't an e-mail");
}

// Split the e-mail on the found boundary
// The first part will be the header (hence $i = 1 in our loop)
// Each other chunk should have some info on the chunk,
// then \n\n then the chunk data
// Process each chunk
$email_parts = split("--" . $boundary,$email);
for($i = 1;$i < count($email_parts);$i++){
    process_part($email_parts[$i]);
}

// Put the results in the database if needed
if($save_msg_to_db){
    mysql_connect($db_host,$db_un,$db_pass);
    mysql_select_db($db_name);

    $q = "INSERT INTO `emails` (`from`,`subject`,`body`) VALUES ('" .
	mysql_real_escape_string($from) . "','" .
	mysql_real_escape_string($subject) . "','" .
	mysql_real_escape_string($message) . "')";

    mysql_query($q) or die(mysql_error());

    if(count($files_uploaded) > 0){
	$id = mysql_insert_id();
	$q = "INSERT INTO `files` (`email_id`,`filename`,`size`) VALUES ";
	$filesar = Array();
	foreach($files_uploaded as $f => $s){
	    $filesar[] = "('$id','" .
		mysql_real_escape_string($f) . "','" .
		mysql_real_escape_string($s) . "')";
	}
	$q .= implode(', ',$filesar);
	mysql_query($q) or die(mysql_error());
    }
}

// Send response e-mail if needed
if($send_email && $from != ""){
    $to = $from;
    $newmsg = "Thanks! I just uploaded the following ";
    $newmsg .= "files to your storage:\n\n";
    $newmsg .= "Filename -- Size\n";
    foreach($files_uploaded as $f => $s){
	$newmsg .= "$f -- $s\n";
    }
    $newmsg .= "\nI hope everything looks right. If not,";
    $newmsg .=  "please send me an e-mail!\n";

    mail($to,$subject,$newmsg);
}

Testing The Script:

Save an e-mail, headers and all, and upload it to your server. Cat the saved e-mail to your script to test it.

cat 'saved_email.txt' | ./process_email.php

You should get an e-mail response, see new entries in your DB and see your saved attachments. If you don’t, you can print debugging statements or use your usual PHP debugging techniques with this easy testing method.

Security:

Don’t let anonymous people upload files to your server without appropriate precautions! If someone uploaded a PHP file they could run it and easily gain access to your server that way. Protect the upload destination directory and limit who can send e-mail to this script.  There are probably a bunch of glaring security holes in this script. In my limited use situation where only two people knows the only address that can send e-mail to this script and the address to send it to, and where only I have access to the uploaded files (outside of the server root!) I’m comfortable with the level of security. For your situation, you might need to be more cautious.

EDIT: Sending Email to the Script

Initially I didn’t have any instructions on how to forward email to the script you just made. I have now posted those instructions here: Add an Email Address That Forwards to a Script. It has instructions for CPanel and a link for Exim, Sendmail and Qmail users.

Enjoy!

This entry was posted in Programming, Projects and tagged , , , , , , , . Bookmark the permalink.

59 Responses to Recieve E-mail and Save Attachments with a PHP script

  1. Bernard says:

    Great solution, exactly what I was searching for!

    But there is no instructions on how to send an email to the script in the first place?

    And can a cron job be used to check a mailbox? And if a new email is found, execute the script to process it?

    Thanks

  2. Pingback: Add an email address that forwards to a script | Stuporglue.org

  3. adhie says:

    I can’t get the body and attachment, just empty string, that’s all!
    how to get it? any update from your script?
    thx

  4. stuporglue says:

    Can you send me a sample email that’s not working (full headers and
    everything). Either as an attachment or directly to
    stuporglue@gmail.com would be fine. My script has only been tested on
    email from a couple of mail clients, so there may be headers that
    will break it.

    If you’ll send me a sample I can take a look at it.

  5. Andy says:

    Just what I am looking for – absolutely brilliant thank you! Just one snag… At present, it appears that the body and any attachments are not being stored in the database / saved as files. Any thoughts? Would very much appreciate some tips.

    thanks again!

  6. Marko says:

    Hello,
    Everything works fine except saving attachments. And I can’t find any errors. Any idea what could it be?

    • stuporglue says:

      If I had to guess (for both Andy’s and Marko’s problems) I would suggest checking folder permissions. Make sure that the user who runs the script has permissions to write to the designated folder, and that the database connection info and table structure are correct. Note that the script is probably run as the mail user.

  7. Jess says:

    Nice! just one quick question, how would the code differ incase there are multiple “boundary=” preg_match(“/boundary=(.*)$/”,$line,$matches))? I believe this only looks for first boundary= but emails with multiple sections i believe comes with multiple boundary=. thanks.

    • stuporglue says:

      It should handle as many parts as you’ve got.

      Here’s the relevant snippet again:

      function process_multipart(&$info,&$data){
      ...
      $multi_parts = split("--" .$bounds,$data);
      for($i = 1;$i < count($multi_parts);$i++){
      process_part($multi_parts[$i]);
      }
      }

      We do a split on the data using $bounds as the delimiter , so the data becomes an array ($multi_parts) with each array item being one bounded part of data. That part is then sent off to process_part to discover what it is and save it off if appropriate.

  8. Alex says:

    Hi im really new on programming, i follow your instructions, but im getting this mail back.. with this message.

    A message that you sent could not be delivered to one or more of its
    recipients. This is a permanent error. The following address(es) failed:

    pipe to |/home/paelladelicatessen/buzon/process_email.php

    Any tip any help i would appreciate.

    thanks

  9. Hlavco says:

    Just as a hint to anyone, you might want to use an absolute path to your save directory. I tried relative first and it didn’t work for me.

    • stuporglue says:

      An absolute path is a good practice.

      Technically a relative path will work since we’re just using fopen, but it must be relative to the directory in which the script is running (which is always the case with fopen and PHP). Which directory that is will depend on how the mail forwarder invokes the PHP script. Since the mail program is most likely running as a daemon, it’s probably running in / and will most likely start the PHP script in / as well.

      • Peter says:

        Whoooohoo! I love it when a plan comes together.

        2 issues i cam acrooss

        1- denying my email adress i had to fully add my name in ‘my name ‘ it did not like ‘myname@kula.org.uk’

        2-the directory issue- each hosting alsways has some wierd chrooting and realtivlaty like you said- but i looked in the log.file you so kindly gerenated and copied used that as my realtive directory– it had extra /home/myaccname/www_root/ instead of just /www_root/

        Excellent script ! Thanks!

  10. Roger says:

    It only works for me when i send an email from my gmail account :O He saves the attachements perfectly.

    But when i send an email via my email accounts in Thunderbird or my online Hotmail account or iPhone (also gmail through iPhone mail app) he doesn’t save the files! I also tried to send mails from the same accounts via a webmail enviroment.

    How can i fix this? :O some setting in my email?

    • stuporglue says:

      You will probably need to look at the raw email that is being sent and fix the parsing that is being done here to work with the emails you are sending.

      A better option (and what I should’ve done originally) might be to use a Perl or PHP email library to do the parsing.

      Good luck!

  11. anaa says:

    hi I followed your instructions but I’m getting this mail back
    This message was created automatically by mail delivery software.

    A message that you sent could not be delivered to one or more of its
    recipients. This is a permanent error. The following address(es) failed:

    pipe to |/home/affiza/public_html/email_script.php

    Please help. thanks in advance

    • stuporglue says:

      It looks like you are sending an email to “pipe to |/home/affiza/public_html/email_script.php” as though it were an email address.

      Instead, you will need to set up an email account on your server (eg. my_file_upload@example.com) and configure the email server to pipe (http://en.wikipedia.org/wiki/Pipeline_%28Unix%29) the emails that are recieved to your PHP script.

      Due to the number of different server setups, you’d need to consult your hosting provider for specific instructions on how to do that step.

      • anaa says:

        I have a setup an email account on my server and allowing it to forward all emails to the provided php sript and in script I have followed all instructions of yours

      • stuporglue says:

        Sorry, I don’t know. The previous user with the similar problem (Alex) didn’t have the path to his script setup correctly, but that’s a pretty simple error which you should be able to catch.

        One possibility is that your web/SSH access is chrooted (or similar), but the email is not, which could lead to a different path being the correct path.

  12. anaa says:

    OK I have fixed that problem by adding a file exim.conf in root/etc folder
    contents of that file are
    # This transport is used for handling pipe deliveries generated by alias
    # or .forward files. If the pipe generates any standard output, it is returned
    # to the sender of the message as a delivery error. Set return_fail_output
    # instead of return_output if you want this to happen only when the pipe fails
    # to complete normally. You can set different transports for aliases and
    # forwards if you want to – see the references to address_pipe in the directors
    # section below.

    address_pipe:
    driver = pipe
    return_fail_output

    virtual_address_pipe:
    driver = pipe
    group = nobody
    return_fail_output
    user = “${lookup{$domain}lsearch* {/etc/virtual/domainowners}{$value}}”

    now the second problem is I’m unable to store any attachment in the specified folder :( where am I going wrong. Please help

    Thanks

    • stuporglue says:

      Make sure that the folder’s permissions are writable by the user the script is running as.

      In your case, I believe that the group will be nobody, and the user will be whatever

      user = “${lookup{$domain}lsearch* {/etc/virtual/domainowners}{$value}}”

      evaluates to.

      On my server I think the script gets run by the mail server user.

      For testing only (because it’s a security risk) you could change the permissions to 777, see if it gets written, and then you’d know if paths are right.

      You could also add
      print `whoami`;
      to the top of the PHP script and change your exim.conf
      #...Set return_fail_output
      # instead of return_output if you want this to happen only when the pipe fails
      # to complete normally.

      This would send the results of `whoami` back to you (also do “print getcwd();”, etc. to figure out what’s going on.

      Don’t forget to fix your permissions when you’re done.

  13. Russell Harrower says:

    I found that it works fine from gmail.com but when sending attachments from mac mail it does not work. I’ll test in outlook tomorrow.

  14. Jordan says:

    Is there a way to save the attachment in a subfolder with the name of the subject field from the email?

    • stuporglue says:

      “Is there a way to save the attachment in a subfolder with the name of the subject field from the email?”

      Sure thing. You’ve got full control over the code and the parsing of the email. You can save it wherever you want.

      • Jordan says:

        Thanks.. I figured it out already actually.. I was trying to get it make a folder named the Subject field of the incoming email, if it didn’t already exist and then save the files in there, give links in the reply email and what not…

        I now have another questions/problem..
        Do this not save .pdf attachments or am I doing something wrong? I have tried changing several lines and what not and can not get it to save .pdf’s or .jpeg’s.. those are the two that I’ve noticed so far.. I’m going to try a couple more formats and see too..

      • stuporglue says:

        It doesn’t care about what type of file it is saving (it will happily save .exe .php or other dangerous files to your server!).

        If the Content-Disposition and Content-Transfer-Encoding that it sees are supported it will try to save the file. If the permissions are correct it should successfully save it.

        The switch statement that starts with
        switch($type){
        case 'text/plain':

        will call process_data($name,$encoding,$parts[1]); if the content type is not text/html or multipart/alternative, and if a name is found.

  15. Jordan says:

    Ok I’ve got it saving every type of file format that I’ve thrown at it.. all but .jpeg for some reason..?

    Ideas?

    • stuporglue says:

      Are the jpegs sent as attachments or inline? Some programs can put them inline in the body message — this script doesn’t have support for that.

      Send yourself an email with a non-jpeg attachment and one with a jpeg and compare the raw emails. There must be some difference in how the attachment is included, but off the top of my head I can’t think of anything.

  16. Gary Williams says:

    This script works great.

    One question: I seem to have an issue when an email with multiple attachments is received. It seems that none of the attachments are saved. Emails with a single attachment work just fine.

    Thanks!

    ======

  17. shiva says:

    Hi,

    I tried but couldnot figureout the problem.
    Problems-1: Not an allowed sender

    • stuporglue says:

      The way the code is written requires an exact match for the email address. The email address may include a name component, eg ‘”Michael Moore”

      • shiva says:

        Thanks for your reply,

        Currently I have commented out the allowed sender .
        Now seems like it is working , but still there is some problem.

        Body part and attachment is not working.

        I’m testing it sending emails from outlook.

        What could be the reason behind it ?

        -Shiva

      • stuporglue says:

        It could either be:

        a) An incompatibility with the formatting of the emails being sent and the PHP code. I don’t have outlook, so I haven’t been able to test that format.

        b) File permission or path issues on the server. If the user the script runs as doesn’t have permissions for the directory you’re trying to save to, it won’t be able to save the file.

        c) Something else

        I would guess that (b) is slightly more likely than (a), but (a) is still a good candidate.

  18. Xianan says:

    its pretty cool script.. and exactly what im looking for, I installed it and everything going well, when received email directly save it in database and save attachment file’s in specific folder i set on, and sent back email inform about all files that uploaded successfully..

    just one thing i face it,, when i want to enter to the script link in my website: www.****.net/**/process_email.php .. its shown this msg “Not an allowed Sender”

    there is no way to show all msgs that he received and processed as a list with msg body and attachments?

    • stuporglue says:

      there is no way to show all msgs that he received and processed as a list with msg body and attachments?

      Once the data is in the database and your server you can do anything you want with it using standard MySQL and PHP (or anything else that can speak to MySQL).

      • Xianan says:

        oh, coz of my little knowledge of php/mySQL basics, its would be perfect if u include simple php commands that need to show msgs as a list with their body and attachment in the web

      • stuporglue says:

        You’re going to need more than a couple simple commands to understand what’s going on. Head over to http://tizag.com/phpT/ and dig in. It’ll be good for you. :-)

  19. Craig Hopson says:

    This is a great script got it working in part (no attachments YET) but is putting e-mail details in SQL-email OK but nothing in SQL-file…..
    been getting this error

    PHP Warning: PHP Startup: Unable to load dynamic library
    ‘/usr/lib/php/extensions/no-debug-non-zts-20060613/timezonedb.so’ -
    /usr/lib/php/extensions/no-debug-non-zts-20060613/timezonedb.so: cannot open shared
    object file: No such file or directory in Unknown on line 0

    I have passed it to back to my hosting i’ll up date when they reply

    • stuporglue says:

      PHP command line and PHP’s Apache module often have separate config files. It sounds like something isn’t configured quite right for the command line version.

      • Craig Hopson says:

        Yes still waiting on hosting due to Easter weekend although I’m getting this error it’s I don’t think it’s stopping the attachments from being saved any idea why the attachment side of things isn’t working for me??

    • Craig Hopson says:

      ok the error was due to #!/usr/bin/php -q for my server it should be
      #!/usr/local/bin/php -q this has stopped the error but still only works with G-mail attachments and no other E-mail accounts

  20. kent says:

    Your code works great with GMAIL. Congrats and thanks for sharing

    All attachments save to the database just fine, except when you try to send an email using outlook, iphone or any other email client.. Has anyone figured out what needs to be updated so we can successfully parse all emails received without needing a plugin or .class regardless of the email client used.

  21. Pingback: mailReaer.php — Parse E-mail and Save Attachments PHP, Version 2 » Stuporglue.org

  22. Lucky Dj says:

    Hi, I’m Lucky from Pancasila University Jakarta I’m new in php but I’m very interesting on it. My apache server running on windows and i“v followed your instruction about the project and I want to add it into our web programm. We have an error message while we run those script, can you tell me (email me perhaps) the steps to setting the project until it will run well.
    Thnak You for Your understanding
    ( I’m sory about my english, I’m not good at)

    • stuporglue says:

      Lucky,

      I have never run PHP on Windows. The first problem is going to be setting up the email server to pipe emails through the script. Even on Linux setting this script up correctly is an intermediate task. You need to have some knowledge of PHP, MySQL, file permissions and how your mail client works.

      If you do get it working on Windows please write about it and send me a link. I would be interested to see how it is done — as I said, I haven’t run PHP on Windows.

  23. Mark says:

    My Fix to get this saving attachments from Outlook was to remove the quotes that Outlook adds round the header boundary.

    So this is from Outlook:
    Content-Type: multipart/mixed; boundary=”_004_CBED5AA527778markxigencouk_”

    This is from Gmail:
    Content-Type: multipart/mixed; boundary=20cf303f648cf50a9704c157968e

    I just added this after setting the initial boundary variable:
    $boundary = str_replace(‘”‘, ”, $boundary);

    I’m sure you can modify the preg_match part to ignore the quotes, but this is fine for me.

    Regards,
    Mark

  24. Kevin Nichols says:

    I’m looking for a script where I email an attachment to XXX@XXX.com address and it saves the attachment to an /html/XXXX folder on a webserver.

    Any suggestions?

    Kevin

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current day month ye@r *