TigerZF
🌐Español

45.14. Lectura de mensajes de correo

Zend_Mail puede leer mensajes de correo desde varios almacenamientos de correo locales o remotos. Todos ellos tienen la misma API básica para contar y obtener mensajes, y algunos de ellos implementan interfaces adicionales para características menos comunes. Para ver un resumen de características de los almacenamientos implementados, consulte la siguiente tabla.

Tabla 45.1. Resumen de características de lectura de correo

Característica Mbox Maildir Pop3 IMAP
Tipo de almacenamiento local local remoto remoto
Obtener mensaje
Obtener parte MIME emulado emulado emulado emulado
Carpetas No
Crear mensaje/carpeta No pendiente No pendiente
Indicadores No No
Cuota No No No

45.14.1. Ejemplo sencillo usando Pop3

$mail = new Zend_Mail_Storage_Pop3(array('host'     => 'localhost',
                                         'user'     => 'test',
                                         'password' => 'test'));

echo $mail->countMessages() . " messages found\n";
foreach ($mail as $message) {
    echo "Mail from '{$message->from}': {$message->subject}\n";
}

45.14.2. Abrir un almacenamiento local

Mbox y Maildir son los dos formatos soportados para almacenamientos de correo locales, ambos en sus formas más simples.

Si desea leer desde un archivo Mbox solo necesita indicar el nombre del archivo al constructor de Zend_Mail_Storage_Mbox:

$mail = new Zend_Mail_Storage_Mbox(array('filename' =>
                                             '/home/test/mail/inbox'));

Maildir es muy similar pero necesita un dirname:

$mail = new Zend_Mail_Storage_Maildir(array('dirname' =>
                                                '/home/test/mail/'));

Ambos constructores lanzan una Zend_Mail_Exception si el almacenamiento no puede ser leído.

45.14.3. Abrir un almacenamiento remoto

Para almacenamientos remotos se soportan los dos protocolos más populares: Pop3 e Imap. Ambos necesitan al menos un host y un usuario para conectarse e iniciar sesión. La contraseña por defecto es una cadena vacía, y el puerto por defecto es el indicado en el RFC del protocolo.

// connecting with Pop3
$mail = new Zend_Mail_Storage_Pop3(array('host'     => 'example.com',
                                         'user'     => 'test',
                                         'password' => 'test'));

// connecting with Imap
$mail = new Zend_Mail_Storage_Imap(array('host'     => 'example.com',
                                         'user'     => 'test',
                                         'password' => 'test'));

// example for a none standard port
$mail = new Zend_Mail_Storage_Pop3(array('host'     => 'example.com',
                                         'port'     => 1120
                                         'user'     => 'test',
                                         'password' => 'test'));

Para ambos almacenamientos se soportan SSL y TLS. Si utiliza SSL el puerto por defecto cambia según lo indicado en el RFC.

// examples for Zend_Mail_Storage_Pop3, same works for Zend_Mail_Storage_Imap

// use SSL on different port (default is 995 for Pop3 and 993 for Imap)
$mail = new Zend_Mail_Storage_Pop3(array('host'     => 'example.com',
                                         'user'     => 'test',
                                         'password' => 'test',
                                         'ssl'      => 'SSL'));

// use TLS
$mail = new Zend_Mail_Storage_Pop3(array('host'     => 'example.com',
                                         'user'     => 'test',
                                         'password' => 'test',
                                         'ssl'      => 'TLS'));

Ambos constructores pueden lanzar Zend_Mail_Exception o Zend_Mail_Protocol_Exception (que extiende Zend_Mail_Exception), según el tipo de error.

45.14.4. Obtener mensajes y métodos sencillos

Los mensajes pueden obtenerse después de haber abierto el almacenamiento. Necesita el número de mensaje, que es un contador que comienza en 1 para el primer mensaje. Para obtener el mensaje, se utiliza el método getMessage():

$message = $mail->getMessage($messageNum);

El acceso mediante array también está soportado, pero este método de acceso no admitirá ningún parámetro adicional que pudiera añadirse a getMessage(). Mientras no le importe y pueda vivir con los valores por defecto, puede usar:

$message = $mail[$messageNum];

Para iterar sobre todos los mensajes se implementa la interfaz Iterator:

foreach ($mail as $messageNum => $message) {
    // do stuff ...
}

Para contar los mensajes en el almacenamiento, puede usar el método countMessages() o el acceso mediante array:

// method
$maxMessage = $mail->countMessages();

// array access
$maxMessage = count($mail);

Para eliminar un correo, se utiliza el método removeMessage() o, de nuevo, el acceso mediante array:

// method
$mail->removeMessage($messageNum);

// array access
unset($mail[$messageNum]);

45.14.5. Trabajar con mensajes

Después de obtener los mensajes con getMessage() querrá obtener las cabeceras, el contenido o partes individuales de un mensaje multiparte. Se puede acceder a todas las cabeceras mediante propiedades o el método getHeader() si desea más control o tiene nombres de cabecera inusuales. Los nombres de cabecera se convierten internamente a minúsculas, por lo que la capitalización del nombre de la cabecera en el mensaje de correo no importa. Además, las cabeceras con un guion pueden escribirse en camel-case. Si no se encuentra ninguna cabecera para ninguna de las dos notaciones se lanza una excepción. Para evitar esto se puede usar el método headerExists() para comprobar la existencia de una cabecera.

// get the message object
$message = $mail->getMessage(1);

// output subject of message
echo $message->subject . "\n";

// get content-type header
$type = $message->contentType;

// check if CC isset:
if( isset($message->cc) ) { // or $message->headerExists('cc');
    $cc = $message->cc;
}

Si tiene varias cabeceras con el mismo nombre -por ejemplo, las cabeceras Received- podría querer un array en lugar de una cadena. En este caso, utilice el método getHeader().

// get header as property - the result is always a string,
// with new lines between the single occurrences in the message
$received = $message->received;

// the same via getHeader() method
$received = $message->getHeader('received', 'string');

// better an array with a single entry for every occurrences
$received = $message->getHeader('received', 'array');
foreach ($received as $line) {
    // do stuff
}

// if you don't define a format you'll get the internal representation
// (string for single headers, array for multiple)
$received = $message->getHeader('received');
if (is_string($received)) {
    // only one received header found in message
}

El método getHeaders() devuelve todas las cabeceras como un array con el nombre en minúsculas como clave y el valor como un array para cabeceras múltiples o como una cadena para cabeceras únicas.

// dump all headers
foreach ($message->getHeaders() as $name => $value) {
    if (is_string($value)) {
        echo "$name: $value\n";
        continue;
    }
    foreach ($value as $entry) {
        echo "$name: $entry\n";
    }
}

Si no tiene un mensaje multiparte, obtener el contenido es fácil mediante getContent(). A diferencia de las cabeceras, el contenido solo se obtiene cuando se necesita (obtención tardía).

// output message content for HTML
echo '<pre>';
echo $message->getContent();
echo '</pre>';

Comprobar si un mensaje es multiparte se hace con el método isMultipart(). Si tiene un mensaje multiparte puede obtener una instancia de Zend_Mail_Part con el método getPart(). Zend_Mail_Part es la clase base de Zend_Mail_Message, por lo que tiene los mismos métodos: getHeader(), getHeaders(), getContent(), getPart(), isMultipart() y las propiedades para las cabeceras.

// get the first none multipart part
$part = $message;
while ($part->isMultipart()) {
    $part = $message->getPart(1);
}
echo 'Type of this part is ' . strtok($part->contentType, ';') . "\n";
echo "Content:\n";
echo $part->getContent();

Zend_Mail_Part también implementa RecursiveIterator, lo que facilita recorrer todas las partes. Y para facilitar la salida, también implementa el método mágico __toString(), que devuelve el contenido.

// output first text/plain part
$foundPart = null;
foreach (new RecursiveIteratorIterator($mail->getMessage(1)) as $part) {
    try {
        if (strtok($part->contentType, ';') == 'text/plain') {
            $foundPart = $part;
            break;
        }
    } catch (Zend_Mail_Exception $e) {
        // ignore
    }
}
if (!$foundPart) {
    echo 'no plain text part found';
} else {
    echo "plain text part: \n" . $foundPart;
}

45.14.6. Comprobar indicadores

Maildir e IMAP soportan el almacenamiento de indicadores (flags). La clase Zend_Mail_Storage tiene constantes para todos los indicadores de sistema conocidos de maildir e IMAP, llamados Zend_Mail_Storage::FLAG_<flagname>. Para comprobar indicadores, Zend_Mail_Message tiene un método llamado hasFlag(). Con getFlags() obtendrá todos los indicadores establecidos.

// find unread messages
echo "Unread mails:\n";
foreach ($mail as $message) {
    if ($message->hasFlag(Zend_Mail_Storage::FLAG_SEEN)) {
        continue;
    }
    // mark recent/new mails
    if ($message->hasFlag(Zend_Mail_Storage::FLAG_RECENT)) {
        echo '! ';
    } else {
        echo '  ';
    }
    echo $message->subject . "\n";
}

// check for known flags
$flags = $message->getFlags();
echo "Message is flagged as: ";
foreach ($flags as $flag) {
    switch ($flag) {
        case Zend_Mail_Storage::FLAG_ANSWERED:
            echo 'Answered ';
            break;
        case Zend_Mail_Storage::FLAG_FLAGGED:
            echo 'Flagged ';
            break;

        // ...
        // check for other flags
        // ...

        default:
            echo $flag . '(unknown flag) ';
    }
}

Como IMAP permite indicadores definidos por el usuario o el cliente, podría obtener indicadores que no tengan una constante en Zend_Mail_Storage. En su lugar, se devuelven como cadenas y pueden comprobarse de la misma forma con hasFlag().

// check message for client defined flags $IsSpam, $SpamTested
if (!$message->hasFlag('$SpamTested')) {
    echo 'message has not been tested for spam';
} else if ($message->hasFlag('$IsSpam')) {
    echo 'this message is spam';
} else {
    echo 'this message is ham';
}

45.14.7. Uso de carpetas

Todos los almacenamientos, excepto Pop3, soportan carpetas, también llamadas buzones. La interfaz implementada por todos los almacenamientos que soportan carpetas se llama Zend_Mail_Storage_Folder_Interface. Además, todas estas clases tienen un parámetro opcional adicional llamado folder, que es la carpeta seleccionada después de iniciar sesión, en el constructor.

Para los almacenamientos locales necesita usar clases separadas llamadas Zend_Mail_Storage_Folder_Mbox o Zend_Mail_Storage_Folder_Maildir. Ambas necesitan un parámetro llamado dirname con el nombre del directorio base. El formato para maildir es el definido en maildir++ (con un punto como delimitador por defecto), Mbox es una jerarquía de directorios con archivos Mbox. Si no tiene un archivo Mbox llamado INBOX en su directorio base Mbox necesita establecer otra carpeta en el constructor.

Zend_Mail_Storage_Imap ya soporta carpetas por defecto. Ejemplos para abrir estos almacenamientos:

// mbox with folders
$mail = new Zend_Mail_Storage_Folder_Mbox(array('dirname' =>
                                                    '/home/test/mail/'));

// mbox with a default folder not called INBOX, also works
// with Zend_Mail_Storage_Folder_Maildir and Zend_Mail_Storage_Folder_Imap
$mail = new Zend_Mail_Storage_Folder_Mbox(array('dirname' =>
                                                    '/home/test/mail/',
                                                'folder'  =>
                                                    'Archive'));

// maildir with folders
$mail = new Zend_Mail_Storage_Folder_Maildir(array('dirname' =>
                                                       '/home/test/mail/'));

// maildir with colon as delimiter, as suggested in Maildir++
$mail = new Zend_Mail_Storage_Folder_Maildir(array('dirname' =>
                                                       '/home/test/mail/',
                                                   'delim'   => ':'));

// imap is the same with and without folders
$mail = new Zend_Mail_Storage_Imap(array('host'     => 'example.com',
                                         'user'     => 'test',
                                         'password' => 'test'));

Con el método getFolders($root = null) puede obtener la jerarquía de carpetas comenzando en la carpeta raíz o en la carpeta indicada. Se devuelve como una instancia de Zend_Mail_Storage_Folder, que implementa RecursiveIterator, y todos los hijos son también instancias de Zend_Mail_Storage_Folder. Cada una de estas instancias tiene un nombre local y un nombre global, devueltos por los métodos getLocalName() y getGlobalName(). El nombre global es el nombre absoluto desde la carpeta raíz (incluyendo delimitadores), el nombre local es el nombre dentro de la carpeta padre.

Tabla 45.2. Nombres de carpetas de correo

Nombre global Nombre local
/INBOX INBOX
/Archive/2005 2005
List.ZF.General General

Si utiliza el iterador, la clave del elemento actual es el nombre local. El nombre global también se devuelve mediante el método mágico __toString(). Algunas carpetas pueden no ser seleccionables, lo que significa que no pueden almacenar mensajes y seleccionarlas provoca un error. Esto puede comprobarse con el método isSelectable(). Así resulta muy fácil mostrar todo el árbol en una vista:

$folders = new RecursiveIteratorIterator($this->mail->getFolders(),
                                         RecursiveIteratorIterator::SELF_FIRST);
echo '<select name="folder">';
foreach ($folders as $localName => $folder) {
    $localName = str_pad('', $folders->getDepth(), '-', STR_PAD_LEFT) .
                 $localName;
    echo '<option';
    if (!$folder->isSelectable()) {
        echo ' disabled="disabled"';
    }
    echo ' value="' . htmlspecialchars($folder) . '">'
        . htmlspecialchars($localName) . '</option>';
}
echo '</select>';

La carpeta actualmente seleccionada se devuelve mediante el método getCurrentFolder(). Cambiar de carpeta se hace con el método selectFolder(), que necesita el nombre global como parámetro. Si desea evitar escribir delimitadores también puede usar las propiedades de una instancia de Zend_Mail_Storage_Folder:

// depending on your mail storage and its settings $rootFolder->Archive->2005
// is the same as:
//   /Archive/2005
//  Archive:2005
//  INBOX.Archive.2005
//  ...
$folder = $mail->getFolders()->Archive->2005;
echo 'Last folder was '
   . $mail->getCurrentFolder()
   . "new folder is $folder\n";
$mail->selectFolder($folder);

45.14.8. Uso avanzado

45.14.8.1. Usar NOOP

Si está usando un almacenamiento remoto y tiene tareas largas, podría necesitar mantener la conexión activa mediante noop:

foreach ($mail as $message) {

    // do some calculations ...

    $mail->noop(); // keep alive

    // do something else ...

    $mail->noop(); // keep alive
}

45.14.8.2. Almacenamiento en caché de instancias

Zend_Mail_Storage_Mbox, Zend_Mail_Storage_Folder_Mbox, Zend_Mail_Storage_Maildir y Zend_Mail_Storage_Folder_Maildir implementan los métodos mágicos __sleep() y __wakeup(), lo que significa que son serializables. Esto evita analizar los archivos o el árbol de directorios más de una vez. La desventaja es que su almacenamiento Mbox o Maildir no debería cambiar. Se pueden realizar algunas comprobaciones sencillas, como volver a analizar el archivo Mbox actual si la hora de modificación cambia, o volver a analizar la estructura de carpetas si una carpeta ha desaparecido (lo que igualmente resulta en un error, pero puede buscar otra carpeta después). Es mejor si tiene algo como un archivo de señal para los cambios y lo comprueba antes de usar la instancia en caché.

// there's no specific cache handler/class used here,
// change the code to match your cache handler
$signal_file = '/home/test/.mail.last_change';
$mbox_basedir = '/home/test/mail/';
$cache_id = 'example mail cache ' . $mbox_basedir . $signal_file;

$cache = new Your_Cache_Class();
if (!$cache->isCached($cache_id) ||
    filemtime($signal_file) > $cache->getMTime($cache_id)) {
    $mail = new Zend_Mail_Storage_Folder_Pop3(array('dirname' =>
                                                        $mbox_basedir));
} else {
    $mail = $cache->get($cache_id);
}

// do stuff ...

$cache->set($cache_id, $mail);

45.14.8.3. Extender las clases de protocolo

Los almacenamientos remotos usan dos clases: Zend_Mail_Storage_<Name> y Zend_Mail_Protocol_<Name>. La clase de protocolo traduce los comandos y respuestas del protocolo desde y hacia PHP, como métodos para los comandos o variables con diferentes estructuras para los datos. La otra clase, la principal, implementa la interfaz común.

Si necesita características adicionales del protocolo, puede extender la clase de protocolo y usarla en el constructor de la clase principal. Como ejemplo, suponga que necesitamos golpear (knock) diferentes puertos antes de poder conectarnos a POP3.

class Example_Mail_Exception extends Zend_Mail_Exception
{
}

class Example_Mail_Protocol_Exception extends Zend_Mail_Protocol_Exception
{
}

class Example_Mail_Protocol_Pop3_Knock extends Zend_Mail_Protocol_Pop3
{
    private $host, $port;

    public function __construct($host, $port = null)
    {
        // no auto connect in this class
        $this->host = $host;
        $this->port = $port;
    }

    public function knock($port)
    {
        $sock = @fsockopen($this->host, $port);
        if ($sock) {
            fclose($sock);
        }
    }

    public function connect($host = null, $port = null, $ssl = false)
    {
        if ($host === null) {
            $host = $this->host;
        }
        if ($port === null) {
            $port = $this->port;
        }
        parent::connect($host, $port);
    }
}

class Example_Mail_Pop3_Knock extends Zend_Mail_Storage_Pop3
{
    public function __construct(array $params)
    {
        // ... check $params here! ...
        $protocol = new Example_Mail_Protocol_Pop3_Knock($params['host']);

        // do our "special" thing
        foreach ((array)$params['knock_ports'] as $port) {
            $protocol->knock($port);
        }

        // get to correct state
        $protocol->connect($params['host'], $params['port']);
        $protocol->login($params['user'], $params['password']);

        // initialize parent
        parent::__construct($protocol);
    }
}

$mail = new Example_Mail_Pop3_Knock(array('host'        => 'localhost',
                                          'user'        => 'test',
                                          'password'    => 'test',
                                          'knock_ports' =>
                                              array(1101, 1105, 1111)));

Como puede ver, siempre asumimos que estamos conectados, con la sesión iniciada y, si se admite, con una carpeta seleccionada en el constructor de la clase principal. Por lo tanto, si asigna su propia clase de protocolo, siempre debe asegurarse de que eso se haga o el siguiente método fallará si el servidor no lo permite en el estado actual.

45.14.8.4. Uso de cuota (desde la versión 1.5)

Zend_Mail_Storage_Writable_Maildir tiene soporte para cuotas de Maildir++. Está deshabilitado por defecto, pero es posible usarlo manualmente, si no se desean las comprobaciones automáticas (esto significa que appendMessage(), removeMessage() y copyMessage() no realizan comprobaciones ni añaden entradas al archivo maildirsize). Si está habilitado, se lanza una excepción si intenta escribir en el maildir y este ya está sobre la cuota.

Existen tres métodos usados para las cuotas: getQuota(), setQuota() y checkQuota():

$mail = new Zend_Mail_Storage_Writable_Maildir(array('dirname' =>
                                                   '/home/test/mail/'));
$mail->setQuota(true); // true to enable, false to disable
echo 'Quota check is now ', $mail->getQuota() ? 'enabled' : 'disabled', "\n";
// check quota can be used even if quota checks are disabled
echo 'You are ', $mail->checkQuota() ? 'over quota' : 'not over quota', "\n";

checkQuota() también puede devolver una respuesta más detallada:

$quota = $mail->checkQuota(true);
echo 'You are ', $quota['over_quota'] ? 'over quota' : 'not over quota', "\n";
echo 'You have ',
     $quota['count'],
     ' of ',
     $quota['quota']['count'],
     ' messages and use ';
echo $quota['size'], ' of ', $quota['quota']['size'], ' octets';

Si desea especificar su propia cuota en lugar de usar la indicada en el archivo maildirsize puede hacerlo con setQuota():

// message count and octet size supported, order does matter
$quota = $mail->setQuota(array('size' => 10000, 'count' => 100));

Para añadir sus propias comprobaciones de cuota, use letras individuales como claves, y se conservarán (aunque, obviamente, no se comprobarán). También es posible extender Zend_Mail_Storage_Writable_Maildir para definir su propia cuota solo si falta el archivo maildirsize (lo que puede ocurrir en Maildir++):

class Example_Mail_Storage_Maildir extends Zend_Mail_Storage_Writable_Maildir {
    // getQuota is called with $fromStorage = true by quota checks
    public function getQuota($fromStorage = false) {
        try {
            return parent::getQuota($fromStorage);
        } catch (Zend_Mail_Storage_Exception $e) {
            if (!$fromStorage) {
                // unknown error:
                throw $e;
            }
            // maildirsize file must be missing

            list($count, $size) = get_quota_from_somewhere_else();
            return array('count' => $count, 'size' => $size);
        }
    }
}