I'm writing a help desk pipe handler to pipe incoming e-mails as helpdesk ticket replies. Some e-mails are coming in perfectly fine, others are coming in as a jumble of the text and =3D's all munged into one giant string. Does anyone have an idea on how to decode that into plain text.
For reference, this is my mail parser function:
public function parseEmailMessage(Zend_Mail_Message $msg)
{
if ($msg->isMultiPart()) {
$arrAttachments = array();
$body = '';
// Multipart Mime Message
foreach (new RecursiveIteratorIterator($msg) as $part) {
try {
$mimeType = strtok($part->contentType, ';');
// Parse file name
preg_match('/name="(?<filename>[a-zA-Z0-9.\-_]+)"/is', $part->contentType, $attachmentName);
// Append plaintext results to $body
// All other content parts will be treated as attachments
switch ($mimeType) {
case 'text/plain':
$body .= trim($part->getContent()) . "\n";
break;
case 'text/html':
$body .= trim(strip_tags($part->getContent));
break;
default:
$arrAttachments[] = array(
'attachment_mime' => $mimeType,
'attachment_name' => $this->filterFileName($attachmentName['filename']),
'base64data' => trim($part->getContent())
);
}
} catch (Zend_Mail_Exception $e) {
// ignore
}
}
return array($body, $arrAttachments);
} else {
// Plain text message
return array(trim($msg->getContent()), array());
}
}
I'll take a guess that somehow the content type is not correctly specified and Zend doesn't know how to decode it. I know I've seen this before, but I can't remember where or how it was 'solved'.
It looks like quoted-printable being treated like plain text.
Related
I use codeigniter and imap php currently developing project xampp. For some reason them embedded images not showing.
In my getBody() function I have this call back
$body = preg_replace_callback(
'/src="cid:(.*)">/Uims',
function($m) use($email, $uid){
//split on #
$parts = explode('#', $m[1]);
//replace local path with absolute path
$img = str_replace($parts[0], '', $parts[0]);
return "src='$img'>";
},
$body);
I get error
Question: How can I make sure it gets the images correct for the text/html body etc.
<?php
class Email extends MY_Controller {
private $enc;
private $host;
private $user;
private $pass;
private $mailbox;
private $mbox;
public function __construct() {
parent::__construct();
$this->enc = '/imap/ssl/novalidate-cert';
$this->host = '****';
$this->user = '****'; // email
$this->pass = '****'; // Pass
$this->mailbox = '{' . $this->host . $this->enc . '}';
$this->mbox = imap_open($this->mailbox, $this->user, $this->pass);
}
public function view() {
$this->data['message'] = $this->getBody($this->uri->segment(4));
$this->load->view('template/common/header', $this->data);
$this->load->view('template/common/nav', $this->data);
$this->load->view('template/mail/view', $this->data);
$this->load->view('template/common/footer', $this->data);
imap_close($this->mbox);
}
public function getBody($uid) {
$body = $this->get_part($uid, "TEXT/HTML");
// if HTML body is empty, try getting text body
if ($body == "") {
$body = $this->get_part($uid, "TEXT/PLAIN");
}
$email = $this->user;
//replace cid with full path to image
$body = preg_replace_callback(
'/src="cid:(.*)">/Uims',
function($m) use($email, $uid){
//split on #
$parts = explode('#', $m[1]);
//replace local path with absolute path
$img = str_replace($parts[0], '', $parts[0]);
return "src='$img'>";
},
$body);
return trim(utf8_encode(quoted_printable_decode($body)));
}
private function get_part($uid, $mimetype, $structure = false, $partNumber = false) {
if (!$structure) {
$structure = imap_fetchstructure($this->mbox, $uid);
}
if ($structure) {
if ($mimetype == $this->get_mime_type($structure)) {
if (!$partNumber) {
$partNumber = 1;
}
$text = imap_fetchbody($this->mbox, $uid, $partNumber, FT_PEEK);
switch ($structure->encoding) {
# 7BIT
case 0:
return imap_qprint($text);
# 8BIT
case 1:
return imap_8bit($text);
# BINARY
case 2:
return imap_binary($text);
# BASE64
case 3:
return imap_base64($text);
# QUOTED-PRINTABLE
case 4:
return quoted_printable_decode($text);
# OTHER
case 5:
return $text;
# UNKNOWN
default:
return $text;
}
}
// multipart
if ($structure->type == 1) {
foreach ($structure->parts as $index => $subStruct) {
$prefix = "";
if ($partNumber) {
$prefix = $partNumber . ".";
}
$data = $this->get_part($uid, $mimetype, $subStruct, $prefix . ($index + 1));
if ($data) {
return $data;
}
}
}
}
return false;
}
private function get_mime_type($structure) {
$primaryMimetype = array("TEXT", "MULTIPART", "MESSAGE", "APPLICATION", "AUDIO", "IMAGE", "VIDEO", "OTHER");
if ($structure->subtype) {
return $primaryMimetype[(int)$structure->type] . "/" . $structure->subtype;
}
return "TEXT/PLAIN";
}
}
Introduction
You are trying to convert an Email into a HTML Page.
An Email has multiple parts:
Headers
Text based email
HTML based email
Attachments
In the header you will find the Message-ID as well as other relevant metadata.
In order to convert an Email into a website you have to expose the HTML and the Attachments to the browser.
Each of the Parts has its own headers. When you have a url='cid:Whatever' you have to look for which part of the email hast that Content-Id header.
Serve the Email as a Web Page
You need to find wich Email part contains the HTML Body. Parse it and replace the CID URL's for your http://yourhost/{emailId} you already implemented that part so I will not add how to do it here.
Replace CID URL on HTML - Implementation
This is a prototype that may work for you.
$mailHTML="<html>All your long code here</html>";
$mailId="email.eml";
$generatePartURL=function ($messgeId, $cid) {
return "http://myhost/".$messgeId."/".$cid; //Adapt this url according to your needs
};
$replace=function ($matches) use ($generatePartURL, $mailId) {
list($uri, $cid) = $matches;
return $generatePartURL($mailId, $cid);
};
$mailHTML=preg_replace_callback("/cid:([^'\" \]]*)/", $replace, $mailHTML);
Find the part by CID
http://yourhost/{emailId}/{cid}
Pseudo code:
Load email
Find part by CID
Decode from Base64 or other Encoding used (Check Content-Transfer-Encoding header)
Serve the file as an HTTP Response
Which part has my CID image?
Iterate all email parts looking for the Content-ID header that match your CID value.
--_part_boundery_
Content-Type: image/jpeg; name="filename.jpg"
Content-Description: filename.jpg
Content-Disposition: inline; filename="filename.jpg"; size=39619; creation-date="Thu, 28 Dec 2017 10:53:51 GMT"; modification-date="Thu, 28 Dec 2017 10:53:51 GMT"
Content-ID: <YOUR CID WILL BE HERE>
Content-Transfer-Encoding: base64
Decode the transfer encoding and serve the contents as a regular http file.
Webmail implemented in PHP
RoundCube is a webmail implemented in PHP you can have a look how they do this: https://github.com/roundcube/roundcubemail/blob/master/program/lib/Roundcube/rcube_washtml.php
Note: My code it is not based in this solution.
I've got a basic bot set up that will send text back to the user, now I also want to send the user an audio message. But that I cannot do, here is the code.
I'm also using this https://github.com/Eleirbag89/TelegramBotPHP to send the audio
include("Telegram.php);
define('BOT_TOKEN', 'tokentoken');
define('API_URL', 'https://api.telegram.org/bot'.BOT_TOKEN.'/');
$telegram = new Telegram(BOT_TOKEN);
$content = file_get_contents("php://input");
$update = json_decode($content, true);
$chatID = $update["message"]["chat"]["id"];
$message = $update["message"]["text"];
$reply = sendMessage($message);
$sendto =API_URL."sendmessage?chat_id=".$chatID."&text=".$reply;
file_get_contents($sendto);
function sendMessage(&$string) {
switch ($string) {
case "Hi":
$message = "Hi Back";
sendAudio();
break;
case "Bye":
$message = "Bye Bye";
break;
default:
$message = "Default";
}
return $message
}
func sendAudio() {
$sound = curl_file_create('sampleAudio.mp3', 'audio/mp3');
$newContent = array('chat_id' => $chatID, 'audio' => $sound);
$telegram->sendAudio($newContent);
}
Calling the code outside of the functions works, but than the user gets the file each time they type something. I experimenting so a bit of explanation would be great.
You have some errors:
First line " is unclosed
Last function, you've typed "func" instead of "function"
In sendAudio() you've used $chatID but you've to pass it as a parameter of sendAudio() or to set is as global.
I have been trying to fetch message but unsuccessful.
$body = imap_fetchbody($inbox, $email_id, 0);
the messages without attachments are good and I have output but with attachments
gives some complicated outputs out of which both html and plain message are encoded with some (Content-Type) which is a part of gmail messages
You can use the following code to get the plain text part of a multipart email body:
<?php
//get the body
$body = imap_fetchbody($inbox, $email_id, 0);
//parse the boundary separator
$matches = array();
preg_match('#Content-Type: multipart\/[^;]+;\s*boundary="([^"]+)"#i', $body, $matches);
list(, $boundary) = $matches;
$text = '';
if(!empty($boundary)) {
//split the body into the boundary parts
$emailSegments = explode('--' . $boundary, $body);
//get the plain text part
foreach($emailSegments as $segment) {
if(stristr($segment, 'Content-Type: text/plain') !== false) {
$text = trim(preg_replace('/Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n/is', '', $segment));
break;
}
}
}
echo $text;
?>
$body = imap_fetchbody($inbox, $email_id, 1.0);
this seems to be the only one working for me. I think the first integer in the last parameter represents the section of the email, so if it starts with a zero it will contain all the header information. If it starts with a one then it contains the message information. Then the second integer followed by the period is the section of that section. So when I put zero it shows information, but when I put one or two it doesn't show anything for some emails.
This helped
$body = imap_fetchbody($inbox, $email_id, 1.1);
I am trying to change the filter status for the 'subscriptiontrack' using sendgrid. I think I am sending the headers incorrectly, but not totally sure. Working inside a symfony 1.4 framework.
First I create an object of the header settings
$hdr = new SmtpApiHeader();
$hdr->addFilterSetting('subscriptiontrack', 'enable', 0);
$hdr->as_string();
which sets the filter settings and encodes the string
Then I send it off the email class
sendTestEmail::sendEmail($contents, $mailFrom, $testGroup, $subject, $hdr);
SvaSmtpApiHeader.class.php
class SmtpApiHeader
{
function addFilterSetting($filter, $setting, $value)
{
if (!isset($this->data['filters'])) {
$this->data['filters'] = array();
}
if (!isset($this->data['filters'][$filter])) {
$this->data['filters'][$filter] = array();
}
if (!isset($this->data['filters'][$filter]['settings'])) {
$this->data['filters'][$filter]['settings'] = array();
}
$this->data['filters'][$filter]['settings'][$setting] = $value;
}
function asJSON()
{
$json = json_encode($this->data);
// Add spaces so that the field can be folded
$json = preg_replace('/(["\]}])([,:])(["\[{])/', '$1$2 $3', $json);
return $json;
}
function as_string()
{
$json = $this->asJSON();
$str = "X-SMTPAPI: " . wordwrap($json, 76, "\n ");
return $str;
}
}
myEmail.class.php
<?php
class sendTestEmail
{
public static function sendEmail($contents, $mailFrom, $mailTo, $subject, $sgHeaders = null, $attachments = null)
{
try {
/*
* Load connection for mailer
*/
$connection = Swift_SmtpTransport::newInstance('smtp.sendgrid.net', 465, 'ssl')->setUsername(sfconfig::get('app_sendgrid_username'))->setPassword(sfconfig::get('app_sendgrid_password'));
// setup connection/content
$mailer = Swift_Mailer::newInstance($connection);
$message = Swift_Message::newInstance()->setSubject($subject)->setTo($mailTo);
$message->setBody($contents, 'text/html');
// if contains SMTPAPI header add it
if (null !== $sgHeaders) {
$message->getHeaders()->addTextHeader('X-SMTPAPI', $sgHeaders);
}
// update the from address line to include an actual name
if (is_array($mailFrom) and count($mailFrom) == 2) {
$mailFrom = array(
$mailFrom['email'] => $mailFrom['name']
);
}
// add attachments to email
if ($attachments !== null and is_array($attachments)) {
foreach ($attachments as $attachment) {
$attach = Swift_Attachment::fromPath($attachment['file'], $attachment['mime'])->setFilename($attachment['filename']);
$message->attach($attach);
}
}
// Send
$message->setFrom($mailFrom);
$mailer->send($message);
}
catch (Exception $e) {
throw new sfException('Error sending email out - ' . $e->getMessage());
}
}
}
The email is getting sent properly, but the unsubscribe option is still showing up at the bottom. Is this an issue with the header object or a problem with encoding for the header? Is the variable is still an object when getting added to the headers?
You're misunderstanding how JSON encoding works. Let's take a look at your as_string method:
function as_string()
{
$json = $this->asJSON();
$str = "X-SMTPAPI: " . wordwrap($json, 76, "\n ");
return $str;
}
This would output something to the effect of:
X-SMTPAPI: { "filters": { "subscriptiontrack": { "settings": { "enable": 0 } } } }
You should note that this isn't valid JSON because it is prefixed with "X-SMTPAPI". Instead, you should be calling asJSON, but SwiftMailer doesn't know that.
Try switching the header line to:
$message->getHeaders()->addTextHeader('X-SMTPAPI', $sgHeaders->asJSON());
If that doesn't work, can you give us a dump of:
$headers = $message->getHeaders();
echo $headers->toString();
And have you thought about using the official PHP library instead? https://github.com/sendgrid/sendgrid-php
I can't seem to find a real answer to this problem so here I go:
How do you parse raw HTTP request data in multipart/form-data format in PHP? I know that raw POST is automatically parsed if formatted correctly, but the data I'm referring to is coming from a PUT request, which is not being parsed automatically by PHP. The data is multipart and looks something like:
------------------------------b2449e94a11c
Content-Disposition: form-data; name="user_id"
3
------------------------------b2449e94a11c
Content-Disposition: form-data; name="post_id"
5
------------------------------b2449e94a11c
Content-Disposition: form-data; name="image"; filename="/tmp/current_file"
Content-Type: application/octet-stream
�����JFIF���������... a bunch of binary data
I'm sending the data with libcurl like so (pseudo code):
curl_setopt_array(
CURLOPT_POSTFIELDS => array(
'user_id' => 3,
'post_id' => 5,
'image' => '#/tmp/current_file'),
CURLOPT_CUSTOMREQUEST => 'PUT'
);
If I drop the CURLOPT_CUSTOMREQUEST bit, the request is handled as a POST on the server and everything is parsed just fine.
Is there a way to manually invoke PHPs HTTP data parser or some other nice way of doing this?
And yes, I have to send the request as PUT :)
Edit - please read first: this answer is still getting regular hits 7 years later. I have never used this code since then and do not know if there is a better way to do it these days. Please view the comments below and know that there are many scenarios where this code will not work. Use at your own risk.
--
Ok, so with Dave and Everts suggestions I decided to parse the raw request data manually. I didn't find any other way to do this after searching around for about a day.
I got some help from this thread. I didn't have any luck tampering with the raw data like they do in the referenced thread, as that will break the files being uploaded. So it's all regex. This wasnt't tested very well, but seems to be working for my work case. Without further ado and in the hope that this may help someone else someday:
function parse_raw_http_request(array &$a_data)
{
// read incoming data
$input = file_get_contents('php://input');
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// loop data blocks
foreach ($a_blocks as $id => $block)
{
if (empty($block))
continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE)
{
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $block, $matches);
}
// parse all other fields
else
{
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
$a_data[$matches[1]] = $matches[2];
}
}
Usage by reference (in order not to copy around the data too much):
$a_data = array();
parse_raw_http_request($a_data);
var_dump($a_data);
I used Chris's example function and added some needed functionality, such as R Porter's need for array's of $_FILES. Hope it helps some people.
Here is the class & example usage
<?php
include_once('class.stream.php');
$data = array();
new stream($data);
$_PUT = $data['post'];
$_FILES = $data['file'];
/* Handle moving the file(s) */
if (count($_FILES) > 0) {
foreach($_FILES as $key => $value) {
if (!is_uploaded_file($value['tmp_name'])) {
/* Use getimagesize() or fileinfo() to validate file prior to moving here */
rename($value['tmp_name'], '/path/to/uploads/'.$value['name']);
} else {
move_uploaded_file($value['tmp_name'], '/path/to/uploads/'.$value['name']);
}
}
}
I would suspect the best way to go about it is 'doing it yourself', although you might find inspiration in multipart email parsers that use a similar (if not the exact same) format.
Grab the boundary from the Content-Type HTTP header, and use that to explode the various parts of the request. If the request is very large, keep in mind that you might store the entire request in memory, possibly even multiple times.
The related RFC is RFC2388, which fortunately is pretty short.
I'm surprised no one mentioned parse_str or mb_parse_str:
$result = [];
$rawPost = file_get_contents('php://input');
mb_parse_str($rawPost, $result);
var_dump($result);
http://php.net/manual/en/function.mb-parse-str.php
I haven't dealt with http headers much, but found this bit of code that might help
function http_parse_headers( $header )
{
$retVal = array();
$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
foreach( $fields as $field ) {
if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
if( isset($retVal[$match[1]]) ) {
$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
} else {
$retVal[$match[1]] = trim($match[2]);
}
}
}
return $retVal;
}
From http://php.net/manual/en/function.http-parse-headers.php
Here is a universal solution working with arbitrary multipart/form-data content and tested for POST, PUT, and PATCH:
/**
* Parse arbitrary multipart/form-data content
* Note: null result or null values for headers or value means error
* #return array|null [{"headers":array|null,"value":string|null}]
* #param string|null $boundary
* #param string|null $content
*/
function parse_multipart_content(?string $content, ?string $boundary): ?array {
if(empty($content) || empty($boundary)) return null;
$sections = array_map("trim", explode("--$boundary", $content));
$parts = [];
foreach($sections as $section) {
if($section === "" || $section === "--") continue;
$fields = explode("\r\n\r\n", $section);
if(preg_match_all("/([a-z0-9-_]+)\s*:\s*([^\r\n]+)/iu", $fields[0] ?? "", $matches, PREG_SET_ORDER) === 2) {
$headers = [];
foreach($matches as $match) $headers[$match[1]] = $match[2];
} else $headers = null;
$parts[] = ["headers" => $headers, "value" => $fields[1] ?? null];
}
return empty($parts) ? null : $parts;
}
Update
The function was updated to support arrays in form fields. That is fields like level1[level2] will be translated into proper (multidimensional) arrays.
I've just added a small function to my HTTP20 library, that can help with this. It is made to parse form data for PUT, DELETE and PATCH and add it to respective static variable to simulate $_POST global.
For now it's just for text fields, though, no binary support, since I currently do not have a good use case in my project to properly test it and I'd prefer not to share something I can't test extensively. But if I do get to it at some point - I will update this answer.
Here is the code:
public function multiPartFormParse(): void
{
#Get method
$method = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ?? $_SERVER['REQUEST_METHOD'] ?? null;
#Get Content-Type
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
#Exit if not one of the supported methods or wrong content-type
if (!in_array($method, ['PUT', 'DELETE', 'PATCH']) || preg_match('/^multipart\/form-data; boundary=.*$/ui', $contentType) !== 1) {
return;
}
#Get boundary value
$boundary = preg_replace('/(^multipart\/form-data; boundary=)(.*$)/ui', '$2', $contentType);
#Get input stream
$formData = file_get_contents('php://input');
#Exit if failed to get the input or if it's not compliant with the RFC2046
if ($formData === false || preg_match('/^\s*--'.$boundary.'.*\s*--'.$boundary.'--\s*$/muis', $formData) !== 1) {
return;
}
#Strip ending boundary
$formData = preg_replace('/(^\s*--'.$boundary.'.*)(\s*--'.$boundary.'--\s*$)/muis', '$1', $formData);
#Split data into array of fields
$formData = preg_split('/\s*--'.$boundary.'\s*Content-Disposition: form-data;\s*/muis', $formData, 0, PREG_SPLIT_NO_EMPTY);
#Convert to associative array
$parsedData = [];
foreach ($formData as $field) {
$name = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$2', $field);
$value = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$4', $field);
#Check if we have multiple keys
if (str_contains($name, '[')) {
#Explode keys into array
$keys = explode('[', trim($name));
$name = '';
#Build JSON array string from keys
foreach ($keys as $key) {
$name .= '{"' . rtrim($key, ']') . '":';
}
#Add the value itself (as string, since in this case it will always be a string) and closing brackets
$name .= '"' . trim($value) . '"' . str_repeat('}', count($keys));
#Convert into actual PHP array
$array = json_decode($name, true);
#Check if we actually got an array and did not fail
if (!is_null($array)) {
#"Merge" the array into existing data. Doing recursive replace, so that new fields will be added, and in case of duplicates, only the latest will be used
$parsedData = array_replace_recursive($parsedData, $array);
}
} else {
#Single key - simple processing
$parsedData[trim($name)] = trim($value);
}
}
#Update static variable based on method value
self::${'_'.strtoupper($method)} = $parsedData;
}
Obviously you can safely remove method check and assignment to a static, if you do not those.
Have you looked at fopen("php://input", "r") for parsing the content?
Headers can also be found as $_SERVER['HTTP_*'], names are always uppercased and dashes become underscores, eg $_SERVER['HTTP_ACCEPT_LANGUAGE'].