Hlavní navigace

PHP okénko: Zobrazení pošty na webu

Jakub Vrána 20. 4. 2006

V systému pro zpracování objednávek může být velice užitečné, pokud se u každé objednávky zobrazuje veškerá e-mailová korespondence, která se zákazníkem proběhla. K zajištění této funkčnosti lze přistupovat v zásadě dvěma způsoby.

Při příjmu a odesílání zpráv je zároveň ukládat do databáze

Toto řešení vyžaduje nízkoúrovňové napojení na příjem (např. pomocí .procmailrc) a odesílání zpráv, což může být někdy pracné zařídit a navíc to znamená zprávy ukládat dvakrát – jednou do standardního úložiště a jednou do databáze.

Přistupovat přímo k uložené poště, nejsnáze pomocí IMAP

Protokol IMAP dovoluje ve zprávách snadno prohledávat, např. podle adresy příjemce nebo odesílatele, pokud ale nejsou na poštovním serveru indexy, je vyhledávání ve velkých mailboxech nesnesitelně pomalé (desítky vteřin).

Pokud se tedy budeme chtít vydat touto cestou, budeme si muset informace pro vyhledávání ukládat u sebe a se serverem je synchronizovat. K tomu už je potřeba trochu vědět, jak IMAP funguje:

Zprávy v každém mailboxu mají přiřazené číslo UID, které se v průběhu času nemění a je vzestupné. Nové zprávy dostanou první dosud nepoužité UID. Kdo zná modifikátor AUTO_INCREMENT, tomu je princip dobře známý. Kromě tohoto trvale platného identifikátoru může server vracet také pořadí zprávy v daném kontextu (např. na základě vyhledávání), to ale potřebovat nebudeme.

Plán práce: pro každý adresář si budeme ukládat počet zpráv, které v něm jsou uložené, a hodnotu uidnext, která uchovává UID příští zprávy, která bude do adresáře uložena. Pokud se změní uidnext, tak do adresáře nějaká zpráva přibyla, pokud se změní rozdíl uidnext – počet zpráv, tak z něj byla nějaká zpráva odstraněna. Tuto kontrolu provedeme postupně pro všechny adresáře:

<?php
$root = "{imap.example.org}";
$uidnext = mysql_get_vals("SELECT mailbox, uidnext FROM imap_mailbox");
$messages = mysql_get_vals("SELECT mailbox, messages FROM imap_mailbox");
$imap = imap_open($root, $login, $password, OP_HALFOPEN);

// průchod přes všechny adresáře
foreach (imap_list($imap, $root, "*") as $mailbox) {
    $mailbox_root = substr($mailbox, strlen($root));
    $status = imap_status($imap, $mailbox, SA_UIDNEXT | SA_MESSAGES); // zjištění počtu zpráv a hodnoty uidnext
    if ($status->uidnext != $uidnext[$mailbox_root] || $status->messages != $messages[$mailbox_root]) { // v adresáři došlo ke změně
        imap_reopen($imap, $mailbox);

        // aktualizace počtů
        if (isset($uidnext[$mailbox_root])) {
            mysql_query("
                UPDATE imap_mailbox
                SET uidnext = $status->uidnext, messages = $status->messages
                WHERE mailbox = '" . addslashes($mailbox_root) . "'
            ");
        } else {
            mysql_query("
                INSERT INTO imap_mailbox (mailbox, uidnext, messages)
                VALUES ('" . addslashes($mailbox_root) . "', $status->uidnext, $status->messages)
            ");
        }

        // přidané zprávy
        if ($status->uidnext != $uidnext[$mailbox_root]) {
            foreach (imap_fetch_overview($imap, $uidnext[$mailbox_root] . ":" . ($status->uidnext - 1), FT_UID) as $val) {
                $set = array(
                    "mailbox" => "'" . addslashes($mailbox_root) . "'",
                    "uid" => $val->uid,
                    "subject" => "'" . addslashes(imap_utf8($val->subject)) . "'",
                    "date" => "'" . date("Y-m-d H:i:s", strtotime($val->date)) . "'",
                );
                // uložení všech kombinací příjemců a odesílatelů
                foreach (imap_rfc822_parse_adrlist($val->from) as $from) {
                    $set["addr_from"] = "'" . addslashes("$from->mailbox@$from->host") . "'";
                    foreach (imap_rfc822_parse_adrlist($val->to) as $to) {
                        $set["addr_to"] = "'" . addslashes("$to->mailbox@$to->host") . "'";
                        mysql_query("INSERT INTO imap (" . implode(", ", array_keys($set)) . ") VALUES (" . implode(", ", $set) . ")");
                    }
                }
            }
        }

        // smazané zprávy
        if ($status->messages - $messages[$mailbox_root] != $status->uidnext - $uidnext[$mailbox_root]) {
            mysql_query("
                DELETE FROM imap
                WHERE mailbox = '" . addslashes($mailbox_root) . "' AND uid NOT IN (" . implode(", ", imap_search($imap, "ALL", SE_UID)) . ")
            ");
        }
    }
}

imap_close($imap);
?> 

Protože otevření adresáře funkcí imap_reopen stojí nějaký čas, provádí se jen v případě změny zjištěné funkcí imap_status. Hlavičky všech nových zpráv získáme funkcí imap_fetch_over­view, které předáme seznam požadovaných zpráv ve tvaru $od:$do. Pokud skript zjistí, že z adresáře nějaké zprávy zmizely, smažou se všechny ty, jejichž UID nevrátí funkce imap_search s parametrem ALL. Uživatelská funkcemysql_get_vals vrátí pole, kde klíče tvoří první sloupec a hodnoty druhý.

Zobrazení zpráv

Zprávu zobrazíme funkcí imap_fetchbody, kvůli zprávám v HTML formátu nebo s přílohami ale nejprve budeme muset zjistit její strukturu funkcí imap_fetchstruc­ture. Skript očekává, že ID zobrazované zprávy dostane v parametru  select:

<?php
/** Vrácení první části IMAP zprávy vyhovující požadovanému typu a podtypu
* @param object &$structure struktura zprávy vrácená funkcí imap_fetchstructure(), změní se na strukturu nalezené části zprávy
* @param int [$type] požadovaný typ vrácené části (viz imap_fetchstructure(), např. 0 = text)
* @param string [$subtype] požadovaný podtyp vrácené části (např. "PLAIN"), při předání hodnoty null na něm nezáleží
* @return string identifikátor požadované části použitelný v imap_fetchbody() nebo false v případě nenalezení požadované části
* @copyright Jakub Vrána, http://php.vrana.cz
*/
function imap_first_part(&$structure, $type = 0, $subtype = "PLAIN")
{
    if ($structure->type == $type && (!isset($subtype) || $structure->subtype == $subtype)) {
        return 1;
    } elseif ($structure->parts) {
        foreach ($structure->parts as $key => $val) {
            $return = imap_first_part($val, $type, $subtype);
            if ($return) {
                $structure = $val;
                return ($key + 1) . ($return !== 1 ? ".$return" : "");
            }
        }
    }
    return false;
}

// načtení struktury
$row = mysql_fetch_assoc(mysql_query("SELECT * FROM imap WHERE id = '$_GET[select]'"));
$imap = imap_open("$root$row[mailbox]", $login, $password);
$structure = imap_fetchstructure($imap, $row["uid"], FT_UID);
$part_number = imap_first_part($structure);

// vypsání textové části zprávy
if (!$part_number) {
    echo "Zpráva nemá textovou část.\n";
} else {
    $message = imap_fetchbody($imap, $row["uid"], $part_number, FT_UID | FT_PEEK);
    switch ($structure->encoding) {
        case 3: $message = base64_decode($message); break; // imap_base64
        case 4: $message = quoted_printable_decode($message); break; // imap_qprint
    }
    foreach ($structure->parameters as $parameter) {
        if ($parameter->attribute == 'charset') {
            $message = iconv($parameter->value, 'utf-8', $message);
            break;
        }
    }
    echo nl2br(htmlspecialchars($message));
}
?> 

Tento skript zobrazí první textovou část zprávy. Bylo by možné zobrazovat i zprávy v HTML formátu, v tom případě je ale tělo zprávy nutné důkladně ošetřit, aby nemohlo dojít ke Cross Site Scriptingu.

Našli jste v článku chybu?

23. 11. 2006 22:26

Martin (neregistrovaný)
Ahoj,
dostal jsem se k tomu, že si dokáži přečíst email, zjistit, jestli email má přílohu. Rád bych přílohu emailu rovnou otevřel ve scriptu a rozparsoval. Poradíte mi, jak by se toho dalo dosáhnout?

20. 4. 2006 0:28

jkt (neregistrovaný)
Pred tim, nez zacnes psat IMAP klienta, si prosim precti patricne RFC, viz $subject. Treba prijdes na to, ze existuje i hodnota UIDVALIDITY...
Podnikatel.cz: Vládu obejde, kvůli EET rovnou do sněmovny

Vládu obejde, kvůli EET rovnou do sněmovny

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

DigiZone.cz: Česká televize mění schéma ČT :D

Česká televize mění schéma ČT :D

120na80.cz: Bojíte se encefalitidy?

Bojíte se encefalitidy?

Lupa.cz: Babiš: E-shopů se EET možná nebude týkat

Babiš: E-shopů se EET možná nebude týkat

Podnikatel.cz: Víme první výsledky doby odezvy #EET

Víme první výsledky doby odezvy #EET

Podnikatel.cz: Prodává přes internet. Kdy platí zdravotko?

Prodává přes internet. Kdy platí zdravotko?

Vitalia.cz: Baletky propagují zdravotní superpostel

Baletky propagují zdravotní superpostel

Lupa.cz: Není sleva jako sleva. Jak obchodům nenaletět?

Není sleva jako sleva. Jak obchodům nenaletět?

Vitalia.cz: Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Podnikatel.cz: Na poslední chvíli šokuje vyjímkami v EET

Na poslední chvíli šokuje vyjímkami v EET

Podnikatel.cz: EET zvládneme, budou horší zákony

EET zvládneme, budou horší zákony

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně