Pues seguimos con nuestro sistema de tiquets, ahora queremos recibir un correo electrónico de un usuario y que se cree un tiquet asociado a él.
Lo primero será instalar el plugin de trac xmlrpc
root@epsilon-noc:/var/trac/capa8/plugins/xmlrpcplugin/0.10# python setup.py bdist_egg root@epsilon-noc:/var/trac/capa8/plugins/xmlrpcplugin/0.10# cd dist/ root@epsilon-noc:/var/trac/capa8/plugins/xmlrpcplugin/0.10/dist# ls TracXMLRPC-0.1-py2.7.egg root@epsilon-noc:/var/trac/capa8/plugins/xmlrpcplugin/0.10/dist# cp TracXMLRPC-0.1-py2.7.egg /var/trac/capa8/plugins/
Ahora lo activamos en trac.ini
[components] tracrpc.* = enabled
Si tenemos el plugin account-manager instalado, tendremos que aplicar un par de configuraciones
[components] trac.web.auth.LoginModule = disabled [account-manager] environ_auth_overwrite = false
Addicionalmente tendremos que activar otro plugin, el httpauthplugin
root@epsilon-noc:/var/trac/capa8/plugins/httpauthplugin/0.10# python setup.py bdist_egg root@epsilon-noc:/var/trac/capa8/plugins/httpauthplugin/0.10# cp dist/TracHTTPAuth-1.1-py2.7.egg /var/trac/capa8/plugins/ root@epsilon-noc:/var/trac/capa8/conf# vi trac.ini [components] httpauth.* = enabled [httpauth] paths = /xmlrpc, /login/xmlrpc
Una vez instalado el plugin vamos a usar un script de php que va a actuar como puente entre nuestro buzón de correo y trac. Para ello será necesario instalar algunos paquetes de php5.
root@epsilon-noc:~# apt-get install php5-imap php5-cli php5-xmlrpc php5-curl
Ahora buscamos un directorio donde crear el script
root@epsilon-noc:~# cd /var/trac/ root@epsilon-noc:/var/trac# mkdir mail2trac root@epsilon-noc:/var/trac# cd mail2trac/ root@epsilon-noc:/var/trac/mail2trac# vi mail2trac.php
y creamos el script .php con éste contenido
#!/usr/bin/php
mailbox)) {
$x = $stat['sender'][0];
$ticket['reporter'] = sprintf('%s@%s',$x->mailbox,$x->host);
}
// Find & Load Text Part(s) to Description
$part_list = Mail::part_list($stat);
foreach ($part_list as $part=>$data) {
// Find First Plain
if ( (empty($ticket['description'])) && ($data['mime-type'] == 'text/plain') ) {
$ticket['description'] = Mail::fetch($mail_i,$part);
if (@$data['mime-encoding'] == 'quoted-printable') {
$ticket['description'] = quoted_printable_decode($ticket['description']);
}
}
// Find First HTML when no plain, convert to plain
if ( (empty($ticket['description'])) && ($data['mime-type'] == 'text/html') ) {
$ticket['description'] = Mail::fetch($mail_i,$part);
if (@$data['mime-encoding'] == 'quoted-printable') {
$ticket['description'] = quoted_printable_decode($ticket['description']);
}
$ticket['description'] = html_entity_decode($ticket['description']);
$ticket['description'] = trim(strip_tags($ticket['description']));
}
}
// Does this Ticket Exist?
if (preg_match('/ #(\d+): (.+)/',$ticket['summary'],$m)) {
// die("Possibily Ticket #{$m[1]}\n");
$chk = @Trac::getTicket($m[1]);
// $chk = Trac::getTicket(244);
if ($chk[3]['summary'] == $m[2]) {
// For Sure that Ticket
$ticket['id'] = $m[1];
}
}
// Update or Add
if (!empty($ticket['id'])) {
$arg = array(
'id' => $ticket['id'],
'comment' => $ticket['description'],
'attributes' => array(),
'notify' => false,
);
$x = Trac::ticketUpdate($arg);
print_r($x);
} else {
$ticket['id'] = Trac::ticketInsert($ticket);
if (empty($ticket['id'])) {
echo "Failed to import ticket from {$ticket['reporter']}\n";
continue;
}
}
// Process for Attachments
$skip_list = array('multipart/alternative','text/plain','text/html');
foreach ($part_list as $part=>$data) {
// echo "Checking {$data['mime-type']}\n";
if (in_array($data['mime-type'],$skip_list)) {
continue;
}
if (preg_match('/(audio|image|video)\/.+/',$data['mime-type'])) {
$a = array(
'ticket' => $ticket['id'],
'filename' => 'Unknown.bin',
'description' => null,
'data' => Mail::fetch($mail_i,$part),
);
if (!empty($data['description'])) {
$a['description'] = $data['description'];
}
if (!empty($data['FILENAME'])) {
$a['filename'] = $data['FILENAME'];
}
if (!empty($data['NAME'])) {
$a['filename'] = $data['NAME'];
}
switch (@$data['mime-encoding']) {
case 'base64':
$a['data'] = base64_decode($a['data']);
break;
case 3:
$list[ "$depth$count" ] = array('mime-type' => 'text/plain');
break;
default:
echo "data:]\n" . print_r($data,true) . "[\n";
die("Unhandled Mime Encoding: {$data['mime-encoding']}\n");
}
$res = Trac::ticketAttach($a);
// echo "res:]\n" . print_r($res,true) . "[\n";
if (empty($res)) {
die("Failed to add attachment\n");
}
} else {
echo "Unhandled Attachment Mime Type: {$data['mime-type']}\n";
}
}
Mail::delete($mail_i);
}
Mail::close();
exit(0);
/**
Mail Functions
*/
class Mail
{
private static $_cfg;
private static $_box;
/**
Open the Mailbox
*/
public static function open($cfg)
{
self::$_cfg = $cfg;
self::$_box = imap_open(self::$_cfg['host'],self::$_cfg['user'],self::$_cfg['pass']);
if (empty(self::$_box)) {
return false;
}
return true;
}
/**
Return the Count of Messages
*/
public static function count()
{
$c = imap_check(self::$_box);
return intval($c->Nmsgs);
}
/**
Returns a combined Stat of the Message
*/
public static function stat($i)
{
$stat = array();
// Base info
$x = imap_fetchstructure(self::$_box,$i);
$stat = array_merge($stat, (array)$x);
// More info
$x = imap_headerinfo(self::$_box,$i);
$stat = array_merge($stat, (array)$x);
// die(print_r($stat,true));
return $stat;
}
/**
Makes Header Array, Or one if K, all lowercase
*/
public static function head($i,$want=null)
{
$ret = array();
// Fetch Header
$buf = imap_fetchheader(self::$_box,$i);
$buf = str_replace("\r\n ",' ',$buf);
// die(print_r($buf,true));
// Parse
// $x = imap_rfc822_parse_headers($head_raw);
if (preg_match_all('/^([\w\-]+):\s+(.+)$/m',$buf,$m)) {
for ($i=0;$i<=count($m[0]);$i++) {
$k = strtolower($m[1][$i]);
$v = $m[2][$i];
// If Exists
if (empty($ret[ $k ])) {
$ret[$k] = $v;
} else {
// Promote to Array?
if (!is_array($ret[$k])) {
$ret[$k] = array($ret[$k]);
} else {
$ret[$k][] = $v;
}
}
}
}
// Specific Item?
if (!empty($want)) {
$ret = $ret[ strtolower($want) ];
}
// die(print_r($ret,true));
return $ret;
}
/**
This Routine Parses Messages into Part Detail Array
Not Robust
*/
public static function part_list($stat,$depth=null)
{
if ($depth === null) {
$depth = null;
}
$count = 1;
// echo "Depth: $depth\n";
$list = array();
foreach ($stat['parts'] as $i=>$chk) {
switch ($chk->type) {
case TYPETEXT: // Text
switch (strtolower($chk->subtype)) {
case 'html':
$list[ "$depth$count" ] = array('mime-type' => 'text/html');
break;
case 'plain':
$list[ "$depth$count" ] = array('mime-type' => 'text/plain');
// Radix::dump($chk,true);
break;
}
// Encoding?
switch ($chk->encoding) {
case ENCQUOTEDPRINTABLE: // quoted-printable
//
$list[ "$depth$count" ]['mime-encoding'] = 'quoted-printable';
}
break;
case TYPEMULTIPART: // Multipart?
switch (strtolower($chk->subtype)) {
case 'alternative':
// Radix::dump($chk,true);
$list[ "1" ] = array('mime-type' => 'multipart/alternative');
$list += self::part_list((array)$chk, "$count." );
break;
default:
Radix::dump($chk);
die("Unknown Multipart?\n");
}
break;
case TYPEIMAGE: // Image, blindly accept
case TYPEAUDIO: // Audio, blindly accept
$x = array('mime-type' => 'image/' . strtolower($chk->subtype));
if ($chk->type == TYPEAUDIO) {
$x = array('mime-type' => 'audio/' . strtolower($chk->subtype));
}
$x = array_merge($x, (array)$chk);
if ( (!empty($x['dparameters'])) && (is_array($x['dparameters'])) ) {
foreach ($x['dparameters'] as $y) {
$x[ $y->attribute ] = $y->value;
}
}
if ( (!empty($x['parameters'])) && (is_array($x['parameters'])) ) {
foreach ($x['parameters'] as $y) {
$x[ $y->attribute ] = $y->value;
}
}
// Encoding?
switch ($chk->encoding) {
case ENCBASE64: // quoted-printable
$x['mime-encoding'] = 'base64';
}
$list[ "$depth$count" ] = $x;
break;
default:
echo print_r($chk,true);
die("Unknown Type: $chk->type\n");
}
$count++;
}
return $list;
}
/**
Fetch a part
*/
public static function fetch($i,$part)
{
$x = imap_fetchbody(self::$_box,$i,$part);
return $x;
}
/**
Delete a Message
*/
public static function delete($i)
{
$x = imap_delete(self::$_box,$i);
return $x;
}
/**
Delete a Message
*/
public static function close($i)
{
imap_expunge(self::$_box);
imap_close(self::$_box,CL_EXPUNGE);
}
}
/**
Trac interface wrapper
*/
class Trac
{
public static function ticketInsert($t)
{
$arg = array();
$arg[] = strval($t['summary']);
$arg[] = strval($t['description']);
$arg[] = array('reporter' => $t['reporter']);
$arg[] = true; // Notify
$req = xmlrpc_encode_request('ticket.create',$arg);
// die(print_r($res,true));
$res = Trac::_curl_exec($req);
// die(print_r($res,true));
return self::_rpc_decode($res['body']);
}
/**
Add an Attachment
@see http://www.perlmonks.org/?node_id=693747
*/
public static function ticketAttach($a)
{
// string ticket.putAttachment(int ticket, string filename, string description, base64 data, boolean replace=True)
$arg = array();
$arg[] = strval($a['ticket']);
$arg[] = strval($a['filename']);
$arg[] = strval($a['description']);
// Convert / Promote this type to a "XML-RPC base64 deal
xmlrpc_set_type($a['data'],'base64');
$arg[] = $a['data'];
$arg[] = true; // replace
$req = xmlrpc_encode_request('ticket.putAttachment',$arg);
// die(print_r($req,true));
$res = Trac::_curl_exec($req);
// die(print_r($res,true));
return self::_rpc_decode($res['body']);
}
/**
Returns the Ticket or null
*/
public static function getTicket($t)
{
$arg = array();
$arg[] = intval($t);
$req = xmlrpc_encode_request('ticket.get',$arg);
// die(print_r($res,true));
$res = Trac::_curl_exec($req);
// die(print_r($res,true));
return @self::_rpc_decode($res['body']);
}
/**
Executes the Single or Multiple Requests
*/
private static function _curl_exec($req)
{
global $cfg;
$ch = curl_init($cfg['trac']);
// Booleans
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIESESSION, false);
curl_setopt($ch, CURLOPT_CRLF, false);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
curl_setopt($ch, CURLOPT_FILETIME, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_NETRC, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_VERBOSE, false);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_USERAGENT, 'mail2trac (http://edoceo.com/creo/mail2trac)');
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
$head = array(
'Content-Type: application/xml',
'Content-Length: ' . strlen($req),
);
curl_setopt($ch, CURLOPT_HTTPHEADER, $head);
return array(
'body' => curl_exec($ch),
'code' => curl_errno($ch),
'fail' => curl_error($ch),
'info' => curl_getinfo($ch),
);
}
/**
Decode the XML-RPC response
*/
private static function _rpc_decode($res)
{
$res = xmlrpc_decode($res);
if ( (!empty($res)) && (@xmlrpc_is_fault($res) == true) ) {
trigger_error("xmlrpc: {$res['faultString']} ({$res['faultCode']})");
}
if ( (is_int($res)) && (intval($res) > 0) ) {
return intval($res);
}
return $res;
}
}
Le damos los permisos 0755
root@epsilon-noc:/var/trac/mail2trac# chmod 0755 mail2trac.php
Ahora lo añadiremos a crontab para que se ejecute cada 5 minutos
root@epsilon-noc:/var/trac/mail2trac# vi /etc/crontab # mail2trac */5 * * * * root php5 /var/trac/mail2trac/mail2trac.php
Es importante que tengamos una cuenta de correo específica para esta tarea, ya que los mails que entran serán borrados y si usamos una cuenta ya existente con antiguos correos se va a crear un tiquet para cada uno de los correos que tengamos en nuestro buzón.