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.