Recibir un correo y publicarlo como tiquet en trac

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.

Fuente

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.