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:
- Saves the e-mail sender, subject and body to a database
- Saves any attachments as files and creates an entry for those files in the database, associated with the e-mail info in #1
- Sends a response back to the sender telling them what files were received and their file sizes
- 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!









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
Pingback: Add an email address that forwards to a script | Stuporglue.org
I can’t get the body and attachment, just empty string, that’s all!
how to get it? any update from your script?
thx
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.
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!
Hello,
Everything works fine except saving attachments. And I can’t find any errors. Any idea what could it be?
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.
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.
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.
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
Hi Alex,
I’m sending you an email.
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.
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.
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!
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?
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!
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
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.
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
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.
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
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.
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.
however the attachments are not saving form what I can see. i get this email back
Thanks! I just uploaded the following files to your storage:
Filename — Size
1326110501_1326110501_Twalk.ly-iPhone.png — 199.28 KB
1326110501_1326110501_tigerwifi.png — 334.51 KB
however the URL that it needs to save to
$save_directory = “/home/telecho/public_html/private/email/attachments”;
seems not to save.
I’d start by checking permissions. The script probably runs with mail server permissions, not with web server permissions.
I only ever used it with Gmail. If you want compatibility with everything, you’ll probably need a better email parser, like maybe the PECL Mailparse library, or perhaps something in Perl.
is there away to save the whole email just incase it does not save correctly like in a blob formate or something.
Yes, PHP has lots of functions and can do pretty much anything you want it to.
I’d probably start with these
or possibly
the error I think is that mail (apple and outlook) send some attachments like images as base 64.
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=”Twalk.ly-iPhone.png”
…long base64 content blob cut…
The problem is that my script is not looking for inline content, only for attached content. With the way you have attached the file, it is inline, hence the
Content-Disposition: inline;
filename=CV-IT.pdf
If you attach it, you would instead see
Content-Disposition: attachment; filename="CV-IT.pdf"The Content-Disposition handling is around line 54-64.
Is there a way to save the attachment in a subfolder with the name of the subject field from the email?
“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.
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..
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.
Ok I’ve got it saving every type of file format that I’ve thrown at it.. all but .jpeg for some reason..?
Ideas?
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.
I used Gmail and attached it just like I’ve attached all my other tests
Beats me. I’d recommend saving off the raw email, then stepping through the code pretending that you’re the PHP interpreter. That’s what I would do. :-)
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!
======
That may be, you may need to modify it to handle multiples.
Hi,
I tried but couldnot figureout the problem.
Problems-1: Not an allowed sender
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”‘
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
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.
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?
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).
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
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. :-)
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
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.
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??
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
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.
Pingback: mailReaer.php — Parse E-mail and Save Attachments PHP, Version 2 » Stuporglue.org
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)
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.