diff --git a/api/api.inc.php b/api/api.inc.php index 48836382..5167dc49 100644 --- a/api/api.inc.php +++ b/api/api.inc.php @@ -2,7 +2,7 @@ /********************************************************************* api.inc.php - File included on every API page...handles security and abuse issues + File included on every API page...handles common includes. Peter Rotich Copyright (c) 2006-2012 osTicket @@ -13,74 +13,9 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -//postfix exit codes see /usr/include/sysexits.h -define('EX_DATAERR', 65); /* data format error */ -define('EX_NOINPUT', 66); /* cannot open input */ -define('EX_UNAVAILABLE', 69); /* service unavailable */ -define('EX_IOERR', 74); /* input/output error */ -define('EX_TEMPFAIL',75); /* temp failure; user is invited to retry */ -define('EX_NOPERM', 77); /* permission denied */ -define('EX_CONFIG', 78); /* configuration error */ - -define('EX_SUCCESS',0); /* success baby */ - -if(!file_exists('../main.inc.php')) exit(EX_CONFIG); +file_exists('../main.inc.php') or die('System Error'); require_once('../main.inc.php'); -if(!defined('INCLUDE_DIR')) exit(EX_CONFIG); - require_once(INCLUDE_DIR.'class.http.php'); require_once(INCLUDE_DIR.'class.api.php'); -define('OSTAPIINC',TRUE); // Define tag that included files can check - -$remotehost=(isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REMOTE_ADDR']))?TRUE:FALSE; -/* API exit helper */ -function api_exit($code,$msg='') { - global $remotehost, $ost; - - if($code!=EX_SUCCESS) { - //Error occured... - $_SESSION['api']['errors']+=1; - $_SESSION['api']['time']=time(); - $ost->logWarning("API error - code #$code", $msg, ($_SESSION['api']['errors']>10)); - //echo "API Error:.$msg"; - } - if($remotehost){ - switch($code) { - case EX_SUCCESS: - Http::response(200,$code,'text/plain'); - break; - case EX_UNAVAILABLE: - Http::response(405,$code,'text/plain'); - break; - case EX_NOPERM: - Http::response(403,$code,'text/plain'); - break; - case EX_DATAERR: - case EX_NOINPUT: - default: - Http::response(416,$code,'text/plain'); - } - } - exit($code); -} - -//Remote hosts need authorization. -$apikey = null; -if($remotehost) { - //Upto 10 consecutive errors allowed...before a 2 minute timeout. - //One more error during timeout and timeout starts a new clock - if($_SESSION['api']['errors']>10 && (time()-$_SESSION['api']['time'])<=2*60) // timeout! - api_exit(EX_NOPERM, 'Remote host ['.$_SERVER['REMOTE_ADDR'].'] in timeout - error #'.$_SESSION['api']['errors']); - - if(!isset($_SERVER['HTTP_X_API_KEY']) || !isset($_SERVER['REMOTE_ADDR'])) - api_exit(EX_NOPERM, 'API key required'); - elseif(!($apikey=API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR'])) - || !$apikey->isActive() - || $apikey->getIPAddr()!=$_SERVER['REMOTE_ADDR']) - api_exit(EX_NOPERM, 'API key not found/active or source IP not authorized'); - - //At this point we know the remote host/IP is allowed. - $_SESSION['api']['errors']=0; //clear errors for the session. -} ?> diff --git a/api/cron.php b/api/cron.php index a8874e3b..2e605713 100644 --- a/api/cron.php +++ b/api/cron.php @@ -2,7 +2,7 @@ /********************************************************************* cron.php - File to handle cron job calls (local and remote). + File to handle LOCAL cron job calls. Peter Rotich Copyright (c) 2006-2012 osTicket @@ -13,9 +13,11 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +if (substr(php_sapi_name(), 0, 3) != 'cli') + die('cron.php only supports local cron jobs - use http -> api/task/cron'); + @chdir(realpath(dirname(__FILE__)).'/'); //Change dir. require('api.inc.php'); -require_once(INCLUDE_DIR.'class.cron.php'); -Cron::run(); -$ost->logDebug('Cron Job','External cron job executed ['.$_SERVER['REMOTE_ADDR'].']'); +require_once(INCLUDE_DIR.'api.cron.php'); +LocalCronApiController::call(); ?> diff --git a/api/http.php b/api/http.php index 646df679..14eeecd4 100644 --- a/api/http.php +++ b/api/http.php @@ -1,8 +1,8 @@ resolve($_SERVER['PATH_INFO']); +$dispatcher = patterns('', + url_post("^/tickets\.(?Pxml|json|email)$", array('api.tickets.php:TicketApiController','create')), + url('^/task/', patterns('', + url_post("^cron$", array('api.cron.php:CronApiController', 'execute')) + )) + ); +# Call the respective function +print $dispatcher->resolve($_SERVER['PATH_INFO']); ?> diff --git a/api/pipe.php b/api/pipe.php index ff23cfa1..7429a636 100644 --- a/api/pipe.php +++ b/api/pipe.php @@ -3,7 +3,7 @@ /********************************************************************* pipe.php - Converts piped emails to ticket. Both local and remote! + Converts piped emails to ticket. Just local - remote must use /api/tickets.email Peter Rotich Copyright (c) 2006-2012 osTicket @@ -14,122 +14,14 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. -ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments. -$apikey = null; -require('api.inc.php'); -require_once(INCLUDE_DIR.'class.mailparse.php'); -require_once(INCLUDE_DIR.'class.email.php'); - -//Make sure piping is enabled! -if(!$cfg->isEmailPipingEnabled()) - api_exit(EX_UNAVAILABLE,'Email piping not enabled - check MTA settings.'); -elseif($apikey && !$apikey->canCreateTickets()) //apikey is ONLY set on remote post - local post don't need a key (for now). - api_exit(EX_NOPERM, 'API key not authorized'); - -//Get the input -$data=isset($_SERVER['HTTP_HOST'])?file_get_contents('php://input'):file_get_contents('php://stdin'); -if(empty($data)){ - api_exit(EX_NOINPUT,'No data'); -} - -//Parse the email. -$parser= new Mail_Parse($data); -if(!$parser->decode()){ //Decode...returns false on decoding errors - api_exit(EX_DATAERR,'Email parse failed ['.$parser->getError()."]\n\n".$data); -} - - - -//Check from address. make sure it is not a banned address. -$fromlist = $parser->getFromAddressList(); -//Check for parsing errors on FROM address. -if(!$fromlist || PEAR::isError($fromlist)){ - api_exit(EX_DATAERR,'Invalid FROM address ['.$fromlist?$fromlist->getMessage():''."]\n\n".$data); -} - -$from=$fromlist[0]; //Default. -foreach($fromlist as $fromobj){ - if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) - continue; - $from=$fromobj; - break; -} - -//TO Address:Try to figure out the email associated with the message. -$tolist = $parser->getToAddressList(); -foreach ($tolist as $toaddr){ - if(($emailId=Email::getIdByEmail($toaddr->mailbox.'@'.$toaddr->host))){ - //We've found target email. - break; - } -} -if(!$emailId && ($cclist=$parser->getCcAddressList())) { - foreach ($cclist as $ccaddr){ - if(($emailId=Email::getIdByEmail($ccaddr->mailbox.'@'.$ccaddr->host))){ - break; - } - } -} -//TODO: Options to reject emails without a matching To address in db? May be it was Bcc? Current Policy: If you pipe, we accept policy -require_once(INCLUDE_DIR.'class.ticket.php'); //We now need this bad boy! +//Only local piping supported via pipe.php +if (substr(php_sapi_name(), 0, 3) != 'cli') + die('pipe.php only supports local piping - use http -> api/tickets.email'); -$var=array(); -$deptId=0; -$name=trim($from->personal,'"'); -if($from->comment && $from->comment[0]) - $name.=' ('.$from->comment[0].')'; -$subj=utf8_encode($parser->getSubject()); -if(!($body=Format::stripEmptyLines($parser->getBody()))) - $body=$subj?$subj:'(EMPTY)'; - -$var['mid']=$parser->getMessageId(); -$var['email']=$from->mailbox.'@'.$from->host; -$var['name']=$name?utf8_encode($name):$var['email']; -$var['emailId']=$emailId?$emailId:$cfg->getDefaultEmailId(); -$var['subject']=$subj?$subj:'[No Subject]'; -$var['message']=utf8_encode(Format::stripEmptyLines($body)); -$var['header']=$parser->getHeader(); -$var['priorityId']=$cfg->useEmailPriority()?$parser->getPriority():0; - -$ticket=null; -if(preg_match ("[[#][0-9]{1,10}]", $var['subject'], $regs)) { - $extid=trim(preg_replace("/[^0-9]/", "", $regs[0])); - if(!($ticket=Ticket::lookupByExtId($extid, $var['email'])) || strcasecmp($ticket->getEmail(), $var['email'])) - $ticket = null; -} - -$errors=array(); -$msgid=0; -if($ticket) { - //post message....postMessage does the cleanup. - if(!($msgid=$ticket->postMessage($var['message'], 'Email',$var['mid'],$var['header']))) - api_exit(EX_DATAERR, 'Unable to post message'); - -} elseif(($ticket=Ticket::create($var, $errors, 'email'))) { // create new ticket. - $msgid=$ticket->getLastMsgId(); -} else { // failure.... - - // report success on hard rejection - if(isset($errors['errno']) && $errors['errno'] == 403) - api_exit(EX_SUCCESS); - - // check if it's a bounce! - if($var['header'] && TicketFilter::isAutoBounce($var['header'])) { - $ost->logWarning('Bounced email', $var['message'], false); - api_exit(EX_SUCCESS); - } - - api_exit(EX_DATAERR, 'Ticket create Failed '.implode("\n",$errors)."\n\n"); -} - -//Ticket created...save attachments if enabled. -if($ticket && $cfg->allowEmailAttachments() && ($attachments=$parser->getAttachments())) { - foreach($attachments as $attachment) { - if($attachment['filename'] && $ost->isFileTypeAllowed($attachment['filename'])) - $ticket->saveAttachment(array('name' => $attachment['filename'], 'data' => $attachment['body']), $msgid, 'M'); - } -} -api_exit(EX_SUCCESS); +ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments. +@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. +require('api.inc.php'); +require_once(INCLUDE_DIR.'api.tickets.php'); +PipeApiController::process(); ?> diff --git a/api/urls.conf.php b/api/urls.conf.php deleted file mode 100644 index b6a9555e..00000000 --- a/api/urls.conf.php +++ /dev/null @@ -1,11 +0,0 @@ -xml|json)$", "create") -); - -?> diff --git a/include/api.cron.php b/include/api.cron.php new file mode 100644 index 00000000..912fff41 --- /dev/null +++ b/include/api.cron.php @@ -0,0 +1,43 @@ +requireApiKey()) || !$key->canExecuteCronJob()) + return $this->exerr(401, 'API key not authorized'); + + $this->run(); + } + + /* private */ + function run() { + global $ost; + + Cron::run(); + + $ost->logDebug('Cron Job','Cron job executed ['.$_SERVER['REMOTE_ADDR'].']'); + $this->response(200,'Completed'); + } +} + +class LocalCronApiController extends CronApiController { + + function response($code, $resp) { + + if($code == 200) //Success - exit silently. + exit(0); + + //On error echo the response (error) + echo $resp; + exit(1); + } + + function call() { + $cron = new LocalCronApiController(); + $cron->run(); + } +} +?> diff --git a/include/api.ticket.php b/include/api.ticket.php deleted file mode 100644 index 7fd5ba71..00000000 --- a/include/api.ticket.php +++ /dev/null @@ -1,75 +0,0 @@ - array("*" => - array("name", "type", "data", "encoding") - ), - "message", "ip", "priorityId" - ); - if ($format == "xml") return array("ticket" => $supported); - else return $supported; - } - - function create($format) { - - if(!($key=$this->getApiKey()) || !$key->canCreateTickets()) - Http::response(401, 'API key not authorized'); - - # Parse request body - $data = $this->getRequest($format); - if ($format == "xml") $data = $data["ticket"]; - - # Pull off some meta-data - $alert = $data['alert'] ? $data['alert'] : true; - $autorespond = $data['autorespond'] ? $data['autorespond'] : true; - $source = $data['source'] ? $data['source'] : 'API'; - - $attachments = $data['attachments'] ? $data['attachments'] : array(); - - # TODO: Handle attachment encoding (base64) - foreach ($attachments as $filename=>&$info) { - if ($info["encoding"] == "base64") { - # XXX: May fail on large inputs. See - # http://us.php.net/manual/en/function.base64-decode.php#105512 - if (!($info["data"] = base64_decode($info["data"], true))) - Http::response(400, sprintf( - "%s: Poorly encoded base64 data", - $info['name'])); - } - $info['size'] = strlen($info['data']); - } - - # Create the ticket with the data (attempt to anyway) - $errors = array(); - $ticket = Ticket::create($data, $errors, $source, $autorespond, - $alert); - - # Return errors (?) - if (count($errors)) { - Http::response(400, "Unable to create new ticket: validation errors:\n" - . Format::array_implode(": ", "\n", $errors)); - } elseif (!$ticket) { - Http::response(500, "Unable to create new ticket: unknown error"); - } - - # Save attachment(s) - foreach ($attachments as &$info) - $ticket->saveAttachment($info, $ticket->getLastMsgId(), "M"); - - # All done. Return HTTP/201 --> Created - Http::response(201, $ticket->getExtId()); - } -} - -?> diff --git a/include/api.tickets.php b/include/api.tickets.php new file mode 100644 index 00000000..7346f2b2 --- /dev/null +++ b/include/api.tickets.php @@ -0,0 +1,140 @@ + array("*" => + array("name", "type", "data", "encoding") + ), + "message", "ip", "priorityId" + ); + + if(!strcasecmp($format, 'email')) + $supported = array_merge($supported, array('header', 'mid', 'emailId', 'ticketId')); + + return $supported; + } + + function create($format) { + + if(!($key=$this->requireApiKey()) || !$key->canCreateTickets()) + return $this->exerr(401, 'API key not authorized'); + + $ticket = null; + if(!strcasecmp($format, 'email')) { + # Handle remote piped emails - could be a reply...etc. + $ticket = $this->processEmail(); + } else { + # Parse request body + $ticket = $this->createTicket($this->getRequest($format)); + } + + if(!$ticket) + return $this->exerr(500, "Unable to create new ticket: unknown error"); + + $this->response(201, $ticket->getExtId()); + } + + /* private helper functions */ + + function createTicket($data) { + + # Pull off some meta-data + $alert = $data['alert'] ? $data['alert'] : true; + $autorespond = $data['autorespond'] ? $data['autorespond'] : true; + $data['source'] = $data['source'] ? $data['source'] : 'API'; + + # Create the ticket with the data (attempt to anyway) + $errors = array(); + $ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert); + # Return errors (?) + if (count($errors)) { + if(isset($errors['errno']) && $errors['errno'] == 403) + return $this->exerr(403, 'Ticket denied'); + else + return $this->exerr( + 400, + "Unable to create new ticket: validation errors:\n" + .Format::array_implode(": ", "\n", $errors) + ); + } elseif (!$ticket) { + return $this->exerr(500, "Unable to create new ticket: unknown error"); + } + + + # Save attachment(s) + if($data['attachments']) + $ticket->importAttachments($data['attachments'], $ticket->getLastMsgId(), 'M'); + + return $ticket; + } + + function processEmail() { + + $data = $this->getEmailRequest(); + if($data['ticketId'] && ($ticket=Ticket::lookup($data['ticketId']))) { + if(($msgid=$ticket->postMessage($data, 'Email'))) + return $ticket; + } + + return $this->createTicket($data); + } + +} + +//Local email piping controller - no API key required! +class PipeApiController extends TicketApiController { + + //Overwrite grandparent's (ApiController) response method. + function response($code, $resp) { + + //Use postfix exit codes - instead of HTTP + switch($code) { + case 201: //Success + $exitcode = 0; + break; + case 400: + $exitcode = 66; + break; + case 401: /* permission denied */ + case 403: + $exitcode = 77; + break; + case 415: + case 416: + case 417: + case 501: + $exitcode = 65; + break; + case 503: + $exitcode = 69; + break; + case 500: //Server error. + default: //Temp (unknown) failure - retry + $exitcode = 75; + } + + //echo "$code ($exitcode):$resp"; + //We're simply exiting - MTA will take care of the rest based on exit code! + exit($exitcode); + } + + function process() { + $pipe = new PipeApiController(); + if(($ticket=$pipe->processEmail())) + return $pipe->response(201, $ticket->getNumber()); + + return $pipe->exerr(416, 'Request failed -retry again!'); + } +} + +?> diff --git a/include/class.api.php b/include/class.api.php index df8cfb0c..f5d2ef5c 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -71,6 +71,10 @@ function canCreateTickets() { return ($this->ht['can_create_tickets']); } + function canExecuteCronjob() { + return ($this->ht['can_exec_cron']); + } + function update($vars, &$errors) { if(!API::save($this->getId(), $vars, $errors)) @@ -125,6 +129,7 @@ function save($id, $vars, &$errors) { $sql=' updated=NOW() ' .',isactive='.db_input($vars['isactive']) .',can_create_tickets='.db_input($vars['can_create_tickets']) + .',can_exec_cron='.db_input($vars['can_exec_cron']) .',notes='.db_input($vars['notes']); if($id) { @@ -156,23 +161,29 @@ function save($id, $vars, &$errors) { * in the database, and methods for parsing and validating data sent in the * API request. */ + class ApiController { + var $apikey; + function requireApiKey() { # Validate the API key -- required to be sent via the X-API-Key # header - if (!isset($_SERVER['HTTP_X_API_KEY']) || !isset($_SERVER['REMOTE_ADDR'])) - Http::response(403, "API key required"); - elseif (!($key=API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR'])) - || !$key->isActive() - || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR']) - Http::response(401, "API key not found/active or source IP not authorized"); + + if(!($key=$this->getApiKey())) + return $this->exerr(401, 'Valid API key required'); + elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR']) + return $this->exerr(401, 'API key not found/active or source IP not authorized'); return $key; } function getApiKey() { - return $this->requireApiKey(); + + if (!$this->apikey && isset($_SERVER['HTTP_X_API_KEY']) && isset($_SERVER['REMOTE_ADDR'])) + $this->apikey = API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']); + + return $this->apikey; } /** @@ -181,20 +192,43 @@ function getApiKey() { * work will be done for XML requests */ function getRequest($format) { - if (!($stream = @fopen("php://input", "r"))) - Http::response(400, "Unable to read request body"); - if ($format == "xml") { - if (!function_exists("xml_parser_create")) - Http::response(500, "XML extension not supported"); - $tree = new ApiXmlDataParser(); - } elseif ($format == "json") { - $tree = new ApiJsonDataParser(); + + $input = (substr(php_sapi_name(), 0, 3) == 'cli')?'php://stdin':'php://input'; + + if (!($stream = @fopen($input, 'r'))) + $this->exerr(400, "Unable to read request body"); + + $parser = null; + switch(strtolower($format)) { + case 'xml': + if (!function_exists('xml_parser_create')) + $this->exerr(501, 'XML extension not supported'); + + $parser = new ApiXmlDataParser(); + break; + case 'json': + $parser = new ApiJsonDataParser(); + break; + case 'email': + $parser = new ApiEmailDataParser(); + break; + default: + $this->exerr(415, 'Unsupported data format'); } - if (!($data = $tree->parse($stream))) - Http::response(400, $tree->lastError()); + + if (!($data = $parser->parse($stream))) + $this->exerr(400, $parser->lastError()); + $this->validate($data, $this->getRequestStructure($format)); + return $data; } + + function getEmailRequest() { + return $this->getRequest('email'); + } + + /** * Structure to validate the request against -- must be overridden to be * useful @@ -216,9 +250,37 @@ function validate($data, $structure, $prefix="") { } elseif (in_array($key, $structure)) { continue; } - Http::response(400, "$prefix$key: Unexpected data received"); + $this->exerr(400, "$prefix$key: Unexpected data received"); } } + + /** + * API error & logging and response! + * + */ + + /* If possible - DO NOT - overwrite the method downstream */ + function exerr($code, $error='') { + global $ost; + + if($error && is_array($error)) + $error = Format::array_implode(": ", "\n", $error); + + //Log the error as a warning - include api key if available. + $msg = $error; + if($_SERVER['HTTP_X_API_KEY']) + $msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n"; + $ost->logWarning("API Error ($code)", $msg, false); + + $this->response($code, $error); //Responder should exit... + return false; + } + + //Default response method - can be overwritten in subclasses. + function response($code, $resp) { + Http::response($code, $resp); + exit(); + } } include_once "class.xml.php"; @@ -232,6 +294,10 @@ function parse($stream) { * XML data types */ function fixup($current) { + + if($current['ticket']) + $current = $current['ticket']; + if (!is_array($current)) return $current; foreach ($current as $key=>&$value) { @@ -295,6 +361,7 @@ function fixup($current) { "data" => $contents, "type" => ($type) ? $type : "text/plain", "name" => key($info)); + # XXX: Handle decoding here?? if (substr($extra, -6) == "base64") $info["encoding"] = "base64"; # Handle 'charset' hint in $extra, such as @@ -302,8 +369,8 @@ function fixup($current) { # Convert to utf-8 since it's the encoding scheme # for the database. Otherwise, assume utf-8 list($param,$charset) = explode('=', $extra); - if ($param == 'charset' && function_exists('iconv')) - $contents = iconv($charset, "UTF-8", $contents); + if ($param == 'charset' && $charset) + $contents = Formart::utf8encode($contents, $charset); } } unset($value); @@ -316,4 +383,39 @@ function fixup($current) { } } +/* Email parsing */ +include_once "class.mailparse.php"; +class ApiEmailDataParser extends EmailDataParser { + + function parse($stream) { + return $this->fixup(parent::parse($stream)); + } + + function fixup($data) { + global $cfg; + + if(!$data) return $data; + + $data['source'] = 'Email'; + + if(!$data['message']) + $data['message'] = $data['subject']?$data['subject']:'(EMPTY)'; + + if(!$data['subject']) + $data['subject'] = '[No Subject]'; + + if(!$data['emailId']) + $data['emailId'] = $cfg->getDefaultEmailId(); + + if($data['email'] && preg_match ('[[#][0-9]{1,10}]', $data['subject'], $matches)) { + if(($tid=Ticket::getIdByExtId(trim(preg_replace('/[^0-9]/', '', $matches[0])), $data['email']))) + $data['ticketId'] = $tid; + } + + if(!$cfg->useEmailPriority()) + unset($data['priorityId']); + + return $data; + } +} ?> diff --git a/include/class.config.php b/include/class.config.php index c7e96697..50a3174c 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -336,10 +336,6 @@ function isEmailPollingEnabled() { return ($this->config['enable_mail_polling']); } - function isEmailPipingEnabled() { - return ($this->config['enable_email_piping']); - } - function allowPriorityChange() { return ($this->config['allow_priority_change']); } @@ -723,7 +719,6 @@ function updateEmailsSettings($vars, &$errors) { .',admin_email='.db_input($vars['admin_email']) .',enable_auto_cron='.db_input(isset($vars['enable_auto_cron'])?1:0) .',enable_mail_polling='.db_input(isset($vars['enable_mail_polling'])?1:0) - .',enable_email_piping='.db_input(isset($vars['enable_email_piping'])?1:0) .',strip_quoted_reply='.db_input(isset($vars['strip_quoted_reply'])?1:0) .',reply_separator='.db_input($vars['reply_separator']) .' WHERE id='.db_input($this->getId()); diff --git a/include/class.format.php b/include/class.format.php index af3fb0c3..38bc1ac1 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -46,6 +46,31 @@ function files($files) { return $result?array_filter($result):$files; } + + /* encode text into desired encoding - taking into accout charset when available. */ + function encode($text, $charset=null, $encoding='utf-8') { + + //Try auto-detecting charset/encoding + if(!$charset && function_exists('mb_detect_encoding')) + $charset = mb_detect_encoding($text); + + //Cleanup - junk + if($charset && in_array(trim($charset), array('default','x-user-defined'))) + $charset = 'ISO-8859-1'; + + if(function_exists('iconv') && $charset) + return iconv($charset, $encoding.'//IGNORE', $text); + elseif(function_exists('iconv_mime_decode')) + return iconv_mime_decode($text, 0, $encoding); + else //default to utf8 encoding. + return utf8_encode($text); + } + + //Wrapper for utf-8 encoding. + function utf8encode($text, $charset=null) { + return Format::enecode($text, $charset, 'utf-8'); + } + function phone($phone) { $stripped= preg_replace("/[^0-9]/", "", $phone); diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 1be504b8..d0b663f6 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -171,7 +171,7 @@ function checkMailbox($folder, $create=false) { } - function decode($encoding, $text) { + function decode($text, $encoding) { switch($encoding) { case 1: @@ -186,38 +186,23 @@ function decode($encoding, $text) { case 4: $text=imap_qprint($text); break; - case 5: - default: - $text=$text; - } + } + return $text; } //Convert text to desired encoding..defaults to utf8 - function mime_encode($text, $charset=null, $enc='utf-8') { //Thank in part to afterburner - - if($charset && in_array(trim($charset), array('default','x-user-defined'))) - $charset = 'ASCII'; - - if(function_exists('iconv') and ($charset or function_exists('mb_detect_encoding'))) { - if($charset) - return iconv($charset, $enc.'//IGNORE', $text); - elseif(function_exists('mb_detect_encoding')) - return iconv(mb_detect_encoding($text, $this->encodings), $enc, $text); - } elseif(function_exists('iconv_mime_decode')) { - return iconv_mime_decode($text, 0, $enc); - } - - return utf8_encode($text); + function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner + return Format::encode($text, $charset, $encoding); } //Generic decoder - resulting text is utf8 encoded -> mirrors imap_utf8 - function mime_decode($text, $enc='utf-8') { + function mime_decode($text, $encoding='utf-8') { $str = ''; $parts = imap_mime_header_decode($text); foreach ($parts as $part) - $str.= $this->mime_encode($part->text, $part->charset, $enc); + $str.= $this->mime_encode($part->text, $part->charset, $encoding); return $str?$str:imap_utf8($text); } @@ -261,7 +246,7 @@ function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=fal $partNumber=$partNumber?$partNumber:1; if(($text=imap_fetchbody($this->mbox, $mid, $partNumber))) { if($struct->encoding==3 or $struct->encoding==4) //base64 and qp decode. - $text=$this->decode($struct->encoding, $text); + $text=$this->decode($text, $struct->encoding); $charset=null; if($encoding) { //Convert text to desired mime encoding... @@ -323,7 +308,7 @@ function getAttachments($part, $index=0) { return array( array( 'name' => $this->mime_decode($filename), - 'mime' => $this->getMimeType($part), + 'type' => $this->getMimeType($part), 'encoding' => $part->encoding, 'index' => ($index?$index:1) ) @@ -387,38 +372,38 @@ function createTicket($mid) { } $emailId = $this->getEmailId(); - - $var['name']=$this->mime_decode($mailinfo['name']); - $var['email']=$mailinfo['email']; - $var['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; - $var['message']=Format::stripEmptyLines($this->getBody($mid)); - $var['header']=$this->getHeader($mid); - $var['emailId']=$emailId?$emailId:$ost->getConfig()->getDefaultEmailId(); //ok to default? - $var['name']=$var['name']?$var['name']:$var['email']; //No name? use email - $var['mid']=$mailinfo['mid']; - - if(!$var['message']) //An email with just attachments can have empty body. - $var['message'] = '(EMPTY)'; + $vars = array(); + $vars['name']=$this->mime_decode($mailinfo['name']); + $vars['email']=$mailinfo['email']; + $vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; + $vars['message']=Format::stripEmptyLines($this->getBody($mid)); + $vars['header']=$this->getHeader($mid); + $vars['emailId']=$emailId?$emailId:$ost->getConfig()->getDefaultEmailId(); //ok to default? + $vars['name']=$vars['name']?$vars['name']:$vars['email']; //No name? use email + $vars['mid']=$mailinfo['mid']; + + if(!$vars['message']) //An email with just attachments can have empty body. + $vars['message'] = '(EMPTY)'; if($ost->getConfig()->useEmailPriority()) - $var['priorityId']=$this->getPriority($mid); + $vars['priorityId']=$this->getPriority($mid); $ticket=null; $newticket=true; //Check the subject line for possible ID. - if($var['subject'] && preg_match ("[[#][0-9]{1,10}]", $var['subject'], $regs)) { + if($vars['subject'] && preg_match ("[[#][0-9]{1,10}]", $vars['subject'], $regs)) { $tid=trim(preg_replace("/[^0-9]/", "", $regs[0])); //Allow mismatched emails?? For now NO. - if(!($ticket=Ticket::lookupByExtId($tid)) || strcasecmp($ticket->getEmail(), $var['email'])) + if(!($ticket=Ticket::lookupByExtId($tid, $vars['email']))) $ticket=null; } $errors=array(); if($ticket) { - if(!($msgid=$ticket->postMessage($var['message'], 'Email', $var['mid'], $var['header']))) + if(!($msgid=$ticket->postMessage($vars, 'Email'))) return false; - } elseif (($ticket=Ticket::create($var, $errors, 'Email'))) { + } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) { $msgid = $ticket->getLastMsgId(); } else { //Report success if the email was absolutely rejected. @@ -426,8 +411,8 @@ function createTicket($mid) { return true; # check if it's a bounce! - if($var['header'] && TicketFilter::isAutoBounce($var['header'])) { - $ost->logWarning('Bounced email', $var['message'], false); + if($vars['header'] && TicketFilter::isAutoBounce($vars['header'])) { + $ost->logWarning('Bounced email', $vars['message'], false); return true; } @@ -442,23 +427,13 @@ function createTicket($mid) { && $struct->parts && ($attachments=$this->getAttachments($struct))) { - //We're just checking the type of file - not size or number of attachments... - // Restrictions are mainly due to PHP file uploads limitations foreach($attachments as $a ) { - if($ost->isFileTypeAllowed($a['name'], $a['mime'])) { - $file = array( - 'name' => $a['name'], - 'type' => $a['mime'], - 'data' => $this->decode($a['encoding'], imap_fetchbody($this->mbox, $mid, $a['index'])) - ); - $ticket->saveAttachment($file, $msgid, 'M'); - } else { - //This should be really a comment on message - NoT an internal note. - //TODO: support comments on Messages and Responses. - $error = sprintf('Attachment %s [%s] rejected because of file type', $a['name'], $a['mime']); - $ticket->postNote('Email Attachment Rejected', $error, 'SYSTEM', false); - $ost->logDebug('Email Attachment Rejected (Ticket #'.$ticket->getExtId().')', $error); - } + $file = array( + 'name' => $a['name'], + 'type' => $a['type'], + 'data' => $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding']) + ); + $ticket->importAttachments(array($file), $msgid, 'M'); } } diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 8d162e52..58adf3ea 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -15,8 +15,8 @@ class.mailparse.php vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -require_once('Mail/mimeDecode.php'); -require_once('Mail/RFC822.php'); +require_once(PEAR_DIR.'Mail/mimeDecode.php'); +require_once(PEAR_DIR.'Mail/RFC822.php'); class Mail_Parse { @@ -164,6 +164,11 @@ function getPart($struct,$ctypepart) { return $data; } + + function mime_encode($text, $charset=null, $encoding='utf-8') { + return Format::encode($text, $charset, $encoding); + } + function getAttachments($part=null){ if($part==null) @@ -173,10 +178,20 @@ function getAttachments($part=null){ && (!strcasecmp($part->disposition,'attachment') || !strcasecmp($part->disposition,'inline') || !strcasecmp($part->ctype_primary,'image'))){ + if(!($filename=$part->d_parameters['filename']) && $part->d_parameters['filename*']) $filename=$part->d_parameters['filename*']; //Do we need to decode? + + $file=array( + 'name' => $filename, + 'type' => strtolower($part->ctype_primary.'/'.$part->ctype_secondary), + 'data' => $this->mime_encode($part->body, $part->ctype_parameters['charset']) + ); + + if(!$this->decode_bodies && $part->headers['content-transfer-encoding']) + $file['encoding'] = $part->headers['content-transfer-encoding']; - return array(array('filename'=>$filename,'body'=>$part->body)); + return array($file); } $files=array(); @@ -216,4 +231,94 @@ function parsePriority($header=null){ function parseAddressList($address){ return Mail_RFC822::parseAddressList($address, null, null,false); } + + function parse($rawemail) { + $parser= new Mail_Parse($rawemail); + return ($parser && $parser->decode())?$parser:null; + } +} + +class EmailDataParser { + var $stream; + var $error; + + function EmailDataParser($stream=null) { + $this->stream = $stream; + } + + function parse($stream) { + + $contents =''; + if(is_resource($stream)) { + while(!feof($stream)) + $contents .= fread($stream, 8192); + + } else { + $contents = $stream; + } + + $parser= new Mail_Parse($contents); + if(!$parser->decode()) //Decode...returns false on decoding errors + return $this->err('Email parse failed ['.$parser->getError().']'); + + $data =array(); + //FROM address: who sent the email. + if(($fromlist = $parser->getFromAddressList()) && !PEAR::isError($fromlist)) { + $from=$fromlist[0]; //Default. + foreach($fromlist as $fromobj) { + if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) continue; + $from = $fromobj; + break; + } + + $data['name'] = trim($from->personal,'"'); + if($from->comment && $from->comment[0]) + $data['name'].= ' ('.$from->comment[0].')'; + + $data['email'] = $from->mailbox.'@'.$from->host; + } + + //TO Address:Try to figure out the email address... associated with the incoming email. + $emailId = 0; + if(($tolist = $parser->getToAddressList())) { + foreach ($tolist as $toaddr) { + if(($emailId=Email::getIdByEmail($toaddr->mailbox.'@'.$toaddr->host))) + break; + } + } + //maybe we got CC'ed?? + if(!$emailId && ($cclist=$parser->getCcAddressList())) { + foreach ($cclist as $ccaddr) { + if(($emailId=Email::getIdByEmail($ccaddr->mailbox.'@'.$ccaddr->host))) + break; + } + } + + $data['subject'] = Format::utf8encode($parser->getSubject()); + $data['message'] = Format::utf8encode(Format::stripEmptyLines($parser->getBody())); + $data['header'] = $parser->getHeader(); + $data['mid'] = $parser->getMessageId(); + $data['priorityId'] = $parser->getPriority(); + $data['emailId'] = $emailId; + + //attachments XXX: worry about encoding?? + $data['attachments'] = $parser->getAttachments(); + + return $data; + } + + function err($error) { + $this->error = $error; + + return false; + } + + function getError() { + return $this->lastError(); + } + + function lastError() { + return $this->error; + } } +?> diff --git a/include/class.osticket.php b/include/class.osticket.php index 89fd3603..8b297869 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -149,7 +149,7 @@ function isFileTypeAllowed($file, $mimeType='') { /* Function expects a well formatted array - see Format::files() It's up to the caller to reject the upload on error. */ - function validateFileUploads(&$files) { + function validateFileUploads(&$files, $checkFileTypes=true) { $errors=0; foreach($files as &$file) { @@ -160,11 +160,12 @@ function validateFileUploads(&$files) { $file['error'] = 'File upload error #'.$file['error']; elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name'])) $file['error'] = 'Invalid or bad upload POST'; - elseif(!$this->isFileTypeAllowed($file)) - $file['error'] = 'Invalid file type for '.$file['name']; + elseif($checkFileTypes && !$this->isFileTypeAllowed($file)) + $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']); elseif($file['size']>$this->getConfig()->getMaxFileSize()) $file['error'] = sprintf('File (%s) is too big. Maximum of %s allowed', - $file['name'], Format::file_size($this->getConfig()->getMaxFileSize())); + Format::htmlchars($file['name']), + Format::file_size($this->getConfig()->getMaxFileSize())); if($file['error']) $errors++; } diff --git a/include/class.ticket.php b/include/class.ticket.php index b9857d39..e68f8d4f 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1379,43 +1379,46 @@ function release() { } //Insert message from client - function postMessage($message,$source='',$emsgid=null,$headers='',$newticket=false){ + function postMessage($vars, $source='', $alerts=true) { global $cfg; - if(!$this->getId()) return 0; - + if(!$vars || !$vars['message']) + return 0; + //Strip quoted reply...on emailed replies if(!strcasecmp($source, 'Email') && $cfg->stripQuotedReply() - && ($tag=$cfg->getReplySeparator()) && strpos($message, $tag)) - list($message)=split($tag, $message); + && ($tag=$cfg->getReplySeparator()) && strpos($vars['message'], $tag)) + list($vars['message']) = split($tag, $vars['message']); # XXX: Refuse auto-response messages? (via email) XXX: No - but kill our auto-responder. + $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW()' .' ,thread_type="M" ' .' ,ticket_id='.db_input($this->getId()) # XXX: Put Subject header into the 'title' field - .' ,body='.db_input(Format::striptags($message)) //Tags/code stripped...meaning client can not send in code..etc + .' ,body='.db_input(Format::striptags($vars['message'])) //Tags/code stripped...meaning client can not send in code..etc .' ,source='.db_input($source?$source:$_SERVER['REMOTE_ADDR']) .' ,ip_address='.db_input($_SERVER['REMOTE_ADDR']); - if(!db_query($sql) || !($msgid=db_insert_id())) return 0; //bail out.... + if(!db_query($sql) || !($msgid=db_insert_id())) + return 0; //bail out.... $this->setLastMsgId($msgid); - - if ($emsgid !== null) { + + if (isset($vars['mid'])) { $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE .' SET message_id='.db_input($msgid) - .', email_mid='.db_input($emsgid) - .', headers='.db_input($headers); + .', email_mid='.db_input($vars['mid']) + .', headers='.db_input($vars['header']); db_query($sql); } - if($newticket) return $msgid; //Our work is done... + if(!$alerts) return $msgid; //Our work is done... $autorespond = true; - if ($autorespond && $headers && TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($headers))) + if ($autorespond && $vars['header'] && TicketFilter::isAutoResponse($vars['header'])) $autorespond=false; $this->onMessage($autorespond); //must be called b4 sending alerts to staff. @@ -1431,7 +1434,7 @@ function postMessage($message,$source='',$emsgid=null,$headers='',$newticket=fal //If enabled...send alert to staff (New Message Alert) if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) { - $msg = $this->replaceVars($msg, array('message' => $message)); + $msg = $this->replaceVars($msg, array('message' => $vars['message'])); //Build list of recipients and fire the alerts. $recipients=array(); @@ -1735,10 +1738,11 @@ function pdfExport($psize='Letter', $notes=false) { } //online based attached files. - function uploadAttachments($files, $refid, $type) { + function uploadAttachments($files, $refid, $type, $checkFileTypes=false) { global $ost; $uploaded=array(); + $ost->validateFileUploads($files, $checkFileTypes); //Validator sets errors - if any foreach($files as $file) { if(!$file['error'] && ($id=AttachmentFile::upload($file)) @@ -1762,8 +1766,44 @@ function uploadAttachments($files, $refid, $type) { return $uploaded; } + /* Wrapper or uploadAttachments + - used on client interface + - file type check is forced + - $_FILES is passed. + */ + function uploadFiles($files, $refid, $type) { + return $this->uploadAttachments(Format::files($files), $refid, $type, true); + } + + /* Emailed & API attachments handler */ + function importAttachments($attachments, $refid, $type, $checkFileTypes=true) { + global $ost; + + if(!$attachments || !is_array($attachments)) return null; + + $files = array(); + foreach ($attachments as &$info) { + //Do error checking... + if ($checkFileTypes && !$ost->isFileTypeAllowed($info)) + $info['error'] = 'Invalid file type (ext) for '.Format::htmlchars($info['name']); + elseif ($info['encoding'] && !strcasecmp($info['encoding'], 'base64')) { + if(!($info['data'] = base64_decode($info['data'], true))) + $info['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($info['name'])); + } + + if($info['error']) { + $this->logNote('File Import Error', $info['error'], 'SYSTEM', false); + } elseif (($id=$this->saveAttachment($info, $refid, $type))) { + $files[] = $id; + } + } + + return $files; + } + + /* - Save attachment to the DB. uploads (above), email or json/xml. + Save attachment to the DB. upload/import (above). @file is a mixed var - can be ID or file hash. */ @@ -2182,7 +2222,7 @@ function create($vars, &$errors, $origin, $autorespond=true, $alertstaff=true) { $dept = $ticket->getDept(); //post the message. - $msgid=$ticket->postMessage($vars['message'],$source,$vars['mid'],$vars['header'],true); + $msgid=$ticket->postMessage($vars , $source, false); // Configure service-level-agreement for this ticket $ticket->selectSLAId($vars['slaId']); diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php index 6e2ffe6e..507753aa 100644 --- a/include/staff/apikey.inc.php +++ b/include/staff/apikey.inc.php @@ -71,13 +71,23 @@ - Enabled Services:: Check applicable API services. All active keys can make cron call. + Services:: Check applicable API services enabled for the key. - > - Can Create Tickets. (XML/JSON/PIPE) + + + + + + diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php index f8674095..b7175a06 100644 --- a/include/staff/settings-emails.inc.php +++ b/include/staff/settings-emails.inc.php @@ -61,19 +61,13 @@   (System administrator's email) - Incoming Emails: For mail fetcher (polling) to work you must set an external cron job or enable auto-cron + Incoming Emails: For mail fetcher (polling) to work you must set an external cron job or enable auto-cron polling Email Polling: > Enable POP/IMAP polling    > - Enable Auto-Cron (Poll based on staff activity - NOT recommended) - - - - Email Piping: - > Enable email piping -   (You pipe we accept policy) + Poll on auto-cron (Poll based on staff activity - NOT recommended) diff --git a/include/upgrader/sql/c0fd16f4-959a00e.patch.sql b/include/upgrader/sql/c0fd16f4-959a00e.patch.sql new file mode 100644 index 00000000..186dfc5b --- /dev/null +++ b/include/upgrader/sql/c0fd16f4-959a00e.patch.sql @@ -0,0 +1,17 @@ +/** + * @version v1.7 + * + * @schema d959a00e55c75e0c903b9e37324fd25d + */ + +-- Add cron exec service +ALTER TABLE `%TABLE_PREFIX%api_key` + ADD `can_exec_cron` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `can_create_tickets`; + +-- Drop email piping settings from config table. +ALTER TABLE `%TABLE_PREFIX%config` + DROP `enable_email_piping`; + +-- update schema signature +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='d959a00e55c75e0c903b9e37324fd25d'; diff --git a/main.inc.php b/main.inc.php index 2dc651c9..2e7691f2 100644 --- a/main.inc.php +++ b/main.inc.php @@ -63,7 +63,7 @@ #Current version && schema signature (Changes from version to version) define('THIS_VERSION','1.7-RC4+'); //Shown on admin panel - define('SCHEMA_SIGNATURE', 'c0fd16f4eaf99b920be9f7fc6ebead32'); //MD5 signature of the db schema. (used to trigger upgrades) + define('SCHEMA_SIGNATURE', 'd959a00e55c75e0c903b9e37324fd25d'); //MD5 signature of the db schema. (used to trigger upgrades) #load config info $configfile=''; if(file_exists(ROOT_DIR.'ostconfig.php')) //Old installs prior to v 1.6 RC5 diff --git a/open.php b/open.php index a7f064fb..e6f5b529 100644 --- a/open.php +++ b/open.php @@ -33,12 +33,8 @@ if(($ticket=Ticket::create($_POST,$errors,SOURCE))){ $msg='Support ticket request created'; //Upload attachments... - if($cfg->allowOnlineAttachments() - && $_FILES['attachments'] - && ($files=Format::files($_FILES['attachments']))) { - $ost->validateFileUploads($files); //Validator sets errors - if any. - $ticket->uploadAttachments($files, $ticket->getLastMsgId(), 'M'); - } + if($cfg->allowOnlineAttachments() && $_FILES['attachments']) + $ticket->uploadFiles($_FILES['attachments'], $ticket->getLastMsgId(), 'M'); //Logged in...simply view the newly created ticket. if($thisclient && $thisclient->isValid()) { diff --git a/setup/scripts/cleanup-codebase.sh b/setup/cli/cleanup-codebase.sh similarity index 98% rename from setup/scripts/cleanup-codebase.sh rename to setup/cli/cleanup-codebase.sh index d9209b91..40586b18 100644 --- a/setup/scripts/cleanup-codebase.sh +++ b/setup/cli/cleanup-codebase.sh @@ -39,6 +39,7 @@ images/ticket_status_title.jpg include/settings.php # Removed in 1.7.0 +api/urls.conf.php images/bg.gif images/fibres.png images/home.gif @@ -57,6 +58,7 @@ images/ticket_status_icon.jpg images/verticalbar.jpg images/view_closed_btn.gif images/view_open_btn.gif +include/api.ticket.php include/class.msgtpl.php include/class.sys.php include/client/index.php diff --git a/setup/scripts/manage.php b/setup/cli/manage.php similarity index 100% rename from setup/scripts/manage.php rename to setup/cli/manage.php diff --git a/setup/scripts/modules/class.module.php b/setup/cli/modules/class.module.php similarity index 100% rename from setup/scripts/modules/class.module.php rename to setup/cli/modules/class.module.php diff --git a/setup/scripts/modules/unpack.php b/setup/cli/modules/unpack.php similarity index 100% rename from setup/scripts/modules/unpack.php rename to setup/cli/modules/unpack.php diff --git a/setup/scripts/package.php b/setup/cli/package.php similarity index 95% rename from setup/scripts/package.php rename to setup/cli/package.php index 22542b6e..9b5ecae5 100755 --- a/setup/scripts/package.php +++ b/setup/cli/package.php @@ -115,6 +115,12 @@ function package($pattern, $destination, $recurse=false, $exclude=false) { # Load the license and documentation package("*.{txt,md}", ""); +#Rename markdown as text TODO: Do html version before rename. +if(($mds = glob("$stage_path/*.md"))) { + foreach($mds as $md) + rename($md, preg_replace('/\.md$/', '.txt', $md)); +} + # Make an archive of the stage folder $version_info = preg_grep('/THIS_VERSION/', explode("\n", file_get_contents("$root/main.inc.php"))); diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/sql/osTicket-mysql.sql index ba980b87..3d14f86d 100644 --- a/setup/inc/sql/osTicket-mysql.sql +++ b/setup/inc/sql/osTicket-mysql.sql @@ -6,6 +6,7 @@ CREATE TABLE `%TABLE_PREFIX%api_key` ( `ipaddr` varchar(64) NOT NULL, `apikey` varchar(255) NOT NULL, `can_create_tickets` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1', + `can_exec_cron` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1', `notes` text, `updated` datetime NOT NULL, `created` datetime NOT NULL, @@ -97,7 +98,6 @@ CREATE TABLE `%TABLE_PREFIX%config` ( `enable_captcha` tinyint(1) unsigned NOT NULL default '0', `enable_auto_cron` tinyint(1) unsigned NOT NULL default '0', `enable_mail_polling` tinyint(1) unsigned NOT NULL default '0', - `enable_email_piping` tinyint(1) unsigned NOT NULL default '0', `send_sys_errors` tinyint(1) unsigned NOT NULL default '1', `send_sql_errors` tinyint(1) unsigned NOT NULL default '1', `send_mailparse_errors` tinyint(1) unsigned NOT NULL default '1', diff --git a/setup/inc/sql/osTicket-mysql.sql.md5 b/setup/inc/sql/osTicket-mysql.sql.md5 index db25b456..d14b842c 100644 --- a/setup/inc/sql/osTicket-mysql.sql.md5 +++ b/setup/inc/sql/osTicket-mysql.sql.md5 @@ -1 +1 @@ -c0fd16f4eaf99b920be9f7fc6ebead32 +d959a00e55c75e0c903b9e37324fd25d diff --git a/setup/scripts/automail.php b/setup/scripts/automail.php new file mode 100755 index 00000000..9160cb81 --- /dev/null +++ b/setup/scripts/automail.php @@ -0,0 +1,80 @@ +#!/usr/bin/php -q + + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +# Configuration: Enter the url and key. That is it. +# url => URL to api/tickets.email e.g http://yourdomain.com/support/api/tickets.email +# key => API's Key (see admin panel on how to generate a key) +# + +$config = array( + 'url'=>'http://yourdomain.com/support/api/tickets.email', + 'key'=>'API KEY HERE' + ); + +#pre-checks +function_exists('file_get_contents') or die('upgrade php >=4.3'); +function_exists('curl_version') or die('CURL support required'); +#read stdin (piped email) +$data=file_get_contents('php://stdin') or die('Error reading stdin. No message'); + +#set timeout +set_time_limit(10); + +#curl post +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, $config['url']); +curl_setopt($ch, CURLOPT_POST, 1); +curl_setopt($ch, CURLOPT_POSTFIELDS, $data); +curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket API Client v1.7'); +curl_setopt($ch, CURLOPT_HEADER, TRUE); +curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Expect:', 'X-API-Key: '.$config['key'])); +curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); +$result=curl_exec($ch); +curl_close($ch); + +//Use postfix exit codes...expected by MTA. +$code = 75; +if(preg_match('/HTTP\/.* ([0-9]+) .*/', $result, $status)) { + switch($status[1]) { + case 201: //Success + $code = 0; + break; + case 400: + $code = 66; + break; + case 401: /* permission denied */ + case 403: + $code = 77; + break; + case 415: + case 416: + case 417: + case 501: + $code = 65; + break; + case 503: + $code = 69; + break; + case 500: //Server error. + default: //Temp (unknown) failure - retry + $code = 75; + } +} + +exit($code); +?> diff --git a/setup/scripts/automail.pl b/setup/scripts/automail.pl new file mode 100755 index 00000000..9f55612b --- /dev/null +++ b/setup/scripts/automail.pl @@ -0,0 +1,60 @@ +#!/usr/bin/perl +####################################################################### +# automail.pl +# +# Perl script used for remote email piping...same as as the PHP version. +# +# Peter Rotich +# Copyright (c) 2006-2013 osTicket +# http://www.osticket.com +# +# Released under the GNU General Public License WITHOUT ANY WARRANTY. +# See LICENSE.TXT for details. +# +# vim: expandtab sw=4 ts=4 sts=4: +####################################################################### + +#Configuration: Enter the url and key. That is it. +# url=> URL to pipe.php e.g http://yourdomain.com/support/api/tickets.email +# key=> API Key (see admin panel on how to generate a key) + +%config = (url => 'http://yourdomain.com/support/api/tickets.email', + key => 'API KEY HERE'); + +#Get piped message from stdin +while () { + $rawemail .= $_; +} + +use LWP::UserAgent; +$ua = LWP::UserAgent->new; + +$ua->agent('osTicket API Client v1.7'); +$ua->default_header('X-API-Key' => $config{'key'}); +$ua->timeout(10); + +use HTTP::Request::Common qw(POST); + +my $enc ='text/plain'; +my $req = (POST $config{'url'}, Content_Type => $enc, Content => $rawemail); +$response = $ua->request($req); + +# +# Process response +# Add exit codes - depending on what your MTA expects. +# By default postfix exit codes are used - which are standard for MTAs. +# + +use Switch; + +$code = 75; +switch($response->code) { + case 201 { $code = 0; } + case 400 { $code = 66; } + case [401,403] { $code = 77; } + case [415,416,417,501] { $code = 65; } + case 503 { $code = 69 } + case 500 { $code = 75 } +} +#print "RESPONSE: ". $response->code. ">>>".$code; +exit $code; diff --git a/setup/scripts/rcron.php b/setup/scripts/rcron.php new file mode 100755 index 00000000..53c2da00 --- /dev/null +++ b/setup/scripts/rcron.php @@ -0,0 +1,52 @@ +#!/usr/bin/php -q + + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +# Configuration: Enter the url and key. That is it. +# url => URL to api/task/cron e.g http://yourdomain.com/support/api/task/cron +# key => API's Key (see admin panel on how to generate a key) +# + +$config = array( + 'url'=>'http://yourdomain.com/support/api/task/cron', + 'key'=>'API KEY HERE' + ); + +#pre-checks +function_exists('curl_version') or die('CURL support required'); + +#set timeout +set_time_limit(30); + +#curl post +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, $config['url']); +curl_setopt($ch, CURLOPT_POST, 1); +curl_setopt($ch, CURLOPT_POSTFIELDS, ''); +curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket API Client v1.7'); +curl_setopt($ch, CURLOPT_HEADER, TRUE); +curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Expect:', 'X-API-Key: '.$config['key'])); +curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); +$result=curl_exec($ch); +curl_close($ch); + +if(preg_match('/HTTP\/.* ([0-9]+) .*/', $result, $status) && $status[1] == 200) + exit(0); + +echo $result; +exit(1); +?> diff --git a/tickets.php b/tickets.php index 4ee69a8e..cc213787 100644 --- a/tickets.php +++ b/tickets.php @@ -40,13 +40,12 @@ if(!$errors) { //Everything checked out...do the magic. - if(($msgid=$ticket->postMessage($_POST['message'],'Web'))) { - if($cfg->allowOnlineAttachments() - && $_FILES['attachments'] - && ($files=Format::files($_FILES['attachments']))) { - $ost->validateFileUploads($files); //Validator sets errors - if any. - $ticket->uploadAttachments($files, $msgid, 'M'); - } + if(($msgid=$ticket->postMessage(array('message'=>$_POST['message']), 'Web'))) { + + //Upload files + if($cfg->allowOnlineAttachments() && $_FILES['attachments']) + $ticket->uploadFiles($_FILES['attachments'], $msgid, 'M'); + $msg='Message Posted Successfully'; } else { $errors['err']='Unable to post the message. Try again';