Zobrazení pošty na webu

Článek vyšel na serveru Root.cz.

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, je mu 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 uidnextpočet zpráv, tak z něj byla nějaká zpráva odstraněna. Pokud se změní hodnota uidvalidity, tak došlo ke změně identifikátorů zprávy a bude nutné je zaindexovat znovu. Tuto kontrolu provedeme postupně pro všechny adresáře:

<?php
$root = "{imap.example.org}";
$imap = imap_open($root, $login, $password, OP_HALFOPEN);

$imap_mailbox = array();
$result = mysql_query("SELECT * FROM imap_mailbox");
while ($row = mysql_fetch_assoc($result)) {
    $imap_mailbox[$row["mailbox"]] = $row;
}
mysql_free_result($result);

// 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_UIDVALIDITY | SA_MESSAGES); // zjištění počtu zpráv a hodnoty uidnext
    $reindex = ($status->uidvalidity != $imap_mailbox[$mailbox_root]["uidvalidity"]);
    if ($reindex || $status->uidnext != $imap_mailbox[$mailbox_root]["uidnext"] || $status->messages != $imap_mailbox[$mailbox_root]["messages"]) { // v adresáři došlo ke změně
        imap_reopen($imap, $mailbox);
        
        // aktualizace počtů
        if (isset($imap_mailbox[$mailbox_root])) {
            mysql_query("UPDATE imap_mailbox SET uidnext = $status->uidnext, uidvalidity = $status->uidvalidity, messages = $status->messages WHERE mailbox = '" . mysql_real_escape_string($mailbox_root) . "'");
            if ($reindex) { // přeindexování zpráv
                mysql_query("DELETE FROM imap WHERE mailbox = '" . mysql_real_escape_string($mailbox_root) . "'");
            }
        } else {
            mysql_query("INSERT INTO imap_mailbox (mailbox, uidnext, uidvalidity, messages) VALUES ('" . mysql_real_escape_string($mailbox_root) . "', $status->uidnext, $status->uidvalidity, $status->messages)");
        }
        
        // přidané zprávy
        if ($status->uidnext != $imap_mailbox[$mailbox_root]["uidnext"]) {
            foreach (imap_fetch_overview($imap, ($reindex ? 1 : $imap_mailbox[$mailbox_root]["uidnext"]) . ":" . ($status->uidnext - 1), FT_UID) as $val) {
                $set = array(
                    "mailbox" => "'" . mysql_real_escape_string($mailbox_root) . "'",
                    "uid" => $val->uid,
                    "subject" => "'" . mysql_real_escape_string(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, "localhost") as $from) {
                    $set["addr_from"] = "'" . mysql_real_escape_string("$from->mailbox@$from->host") . "'";
                    foreach (imap_rfc822_parse_adrlist($val->to, "localhost") as $to) {
                        $set["addr_to"] = "'" . mysql_real_escape_string("$to->mailbox@$to->host") . "'";
                        mysql_query("INSERT INTO imap (" . implode(", ", array_keys($set)) . ") VALUES (" . implode(", ", $set) . ")");
                    }
                }
            }
        }
        
        // smazané zprávy
        if (!$reindex && $status->messages - $imap_mailbox[$mailbox_root]["messages"] != $status->uidnext - $imap_mailbox[$mailbox_root]["uidnext"]) {
            mysql_query("
                DELETE FROM imap
                WHERE mailbox = '" . mysql_real_escape_string($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_overview, 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á funkce mysql_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_fetchstructure. 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 struktura zprávy vrácená funkcí imap_fetchstructure(), změní se na strukturu nalezené části zprávy
* @param int požadovaný typ vrácené části (viz imap_fetchstructure(), např. 0 = text)
* @param string 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, https://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 = " . intval($_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 ENCBASE64: $message = base64_decode($message); break; // imap_base64
        case ENCQUOTEDPRINTABLE: $message = quoted_printable_decode($message); break; // imap_qprint
    }
    foreach ($structure->parameters as $parameter) {
        if (strtolower($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.

Jakub Vrána, Seznámení s oblastí, 19.4.2006, on-line

Diskuse

MiK:

Článek krátký, ale má zajímavý zdroják. ;)
19.4.2006 17:01:13

jardos:

veru veru :)))
19.4.2006 18:18:51

bukaJ:

:-DDD
20.4.2006 02:46:32

Martin:

Co přesně znamená FT_UID?
23.11.2006 22:35:04

ikona Jakub Vrána:

Znamená, že předaný parametr neznamená pořadí zprávy, ale jeho UID.
23.11.2006 22:54:37

Martin:

Díky za radu a kvalitní článek. Jak bych měl postupovat kdybych chtěl přečtený email označit za nepřečtený?
Snažil jsem se v manuálu najít, kde se píše, co znamená již zmiňované FT_UID, ale nějak se mi to nedaří. Neporadil bys mi prosím znovu? Děkuji, Martin.
24.11.2006 22:37:07

ikona Jakub Vrána:

K vymazávání příznaků zpráv slouží funkce imap_clearflag_full().
25.11.2006 02:23:43

pojízdná kočka:

Jakube, zkusila jsem si to pro {pop3.seznam.cz/pop3} a tam je $parameter->attribute 'CHARSET' místo 'charset' - a tak se běh programu do konverze nedostane (ostatní parametry jsou taky velkými písmeny).
28.2.2010 02:08:21

ikona Jakub Vrána:

Díky za upozornění, kód jsem upravil.
28.2.2010 23:24:15

Nick:

Me pop3.seznam.cz nefunguje :-(. Pokousim se pripojit pomoci imap_open("{pop3.seznam.cz:110/pop3/notls}INBOX",$user,$pass) a nevyhodi to ani chybu, ale ani zadnou odpoved..
11.5.2010 12:05:22

Honca:

Já to teď právě řeším tohle.. Zatím moc nechápu, jak tahle IMAP knihovna s POP3 pracuje.. Mám ve schránce kolem 4500 emailů a nemám šanci si vyfiltrovat ani jednu novou zprávu.. prostě to zamrzne na timeoutu asi a víc nic.. pokud to ale vyzkouším na schránce, kde je jen 30 emailů, tak se v mžiku načtou všechny.. i filtr funguje (teda ono se to nazývá kritéria)
9.11.2010 10:26:01

omar:

Mas vubec zapnuty display_errors a error_reporting?
12.1.2014 21:16:47

theo:

Poznámka: tomu switchi s překódováním zprávy by více slušelo použití konstant (ENCBASE64, ENCQUOTEDPRINTABLE)

Jinak jsem narazil na takový nepříjemný problém, ale nevím jestli se to netýká jen PHP 5.3: Konstrukce FT_UID | FT_PEEK produkuje na všech místech, kde se vyskytuje varování, že taková hodnota je pro daný parametr nepřípustná a následně funkce vrátí FALSE. Bohužel jsem nepřišel na to proč a nezdá se, že i ostatní kdo narazili na stejný problém nenašli žádné řešení...
8.12.2010 14:49:31

ikona Jakub Vrána:

Díky za upozornění, opravil jsem to.

Možná to je prostě chyba PHP, zkus http://bugs.php.net.
8.12.2010 18:29:33

skotouc:

Lze imap knihovna nainstalovat do php pod windows?
23.2.2011 22:30:07

ikona Jakub Vrána:

Ano, je součástí běžné distribuce.
24.2.2011 10:08:31

miki:

dobrý den,
prosím mohl by jste mi objasnit jaká tabulka je třeba pro db, jsem začátečník rád bych si na tomto příkladu vyzkoušel na localhostu funkčnost a následně použil. Děkuji za odpověď Mirek
15.5.2011 05:21:11

martin:

Ahoj super clanek chtel ale neslo by vlozit odkaz na sql soubor? dekuji
1.10.2011 11:45:38

papirek:

Super script děkuju, měl bych otázku jak zjistit zda má mail i HTML část a případně ji zobrazit místo Plain, daří se mi to, ale včetně HTML obsahu mi to vypisuje i (zřejmě) nějakou hlavičku mailu ve které je i PLAIN část.
12.6.2012 12:23:16

Gas-O:

Parada, proste tohle jsem hledal ...
13.5.2014 09:10:02

Petr:

Bojoval jsem se správným nastavením dekódování a konverzí do utf 8. Všude spousta informací, ale nic moc nepomohlo. Po dlouhém hledání jsem narazil na tento 13 let starý článek. Původně jsem byl trošku skeptický, přece jen to je hrozně dlouhá doba. Ale částí zdrojáku jsem se inspiroval a super, všechno funguje jak má! Díky moc!
6.4.2019 00:36:22
avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.