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à.

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