„To je přece jasný, ne? Funkcí mail()
. Takový blbý dotazy. Na to přece není potřeba psát celý článek.“ No... ne tak docela. Funkce mail()
je sice vstupním bodem do procesu doručování, ale k tomu, aby byl mail doručitelný a čitelný druhou stranou, je většinou potřeba ještě pár kroků navíc. Jakožto administrátor virtuálního hostingu vržu zuby pokaždé, když mi někdo na server strčí webík posílající maily, ale už neřeší žádnou sanitizaci a konformitu. V lepším případě mail nedojde, v horším pak může být celý server označen jako původce spamu. Takže jak to dělat lépe a radostněji?
Kdysi dávno v jedné předaleké galaxii
Elektronická pošta je jedním z nejstarších užití počítačových sítí vůbec. RFC 821, které popisuje první standard pro Simple Mail Transfer Protocol, vzniklo v roce 1982 a rozhodně nebylo prvním návrhem, jak si posílat psaníčka po drátech. Existují i daleko starší implementace podobných principů, jako třeba RFC 196 - A Mail Box Protocol, který vznikl na samotném počátku 70. let minulého století a ze kterého SMTP vychází. SMTP zmiňuju hlavně proto, že se jedná o protokol, který se s pár obměnami pro přenos pošty používá i dnes, 36 let po jeho vzniku, a hned tak se používat nepřestane. To bohužel znamená, že si s sebou nese spoustu technologického dluhu, který je potřeba pomaloučku a polehoučku odbourávat. Myslím, že pro e-mailovou komunikaci a věci s ní přímo související existuje vůbec nejvíc standardů a specifikací ze všech možných odvětví, která jsou v kompetenci IETF. Namátkou
- RFC 1939 - Post Office Protocol - Version 3
- RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies
- RFC 2046 - Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types
- RFC 2047 - MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
- RFC 2049 - Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples
- RFC 2183 - Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
- RFC 2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations
- RFC 2392 - Content-ID and Message-ID Uniform Resource Locators
- RFC 2595 - Using TLS with IMAP, POP3 and ACAP
- RFC 3207 - SMTP Service Extension for Secure SMTP over Transport Layer Security
- RFC 3461 - Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
- RFC 3463 - Enhanced Mail System Status Codes
- RFC 3501 - Internet Message Access Protocol - VERSION 4rev1
- RFC 4021 - Registration of Mail and MIME Header Fields
- RFC 4954 - SMTP Service Extension for Authentication
- RFC 5034 - The Post Office Protocol (POP3) Simple Authentication and Security Layer (SASL) Authentication Mechanism
- RFC 5248 - A Registry for SMTP Enhanced Mail System Status Codes
- RFC 5321 - Simple Mail Transfer Protocol
- RFC 5322 - Internet Message Format
- RFC 6409 - Message Submission for Mail
- RFC 6530 - Overview and Framework for Internationalized Email
- RFC 6531 - SMTP Extension for Internationalized Email
- RFC 6532 - Internationalized Email Headers
- RFC 6533 - Internationalized Delivery Status and Dispositi on Notifications
- RFC 6855 - IMAP Support for UTF-8
- RFC 6858 - Simplified POP and IMAP Downgrading for Internationalized Email
- RFC 7817 - Transport Layer Security (TLS) Server Identity Check Procedure for Email-Related Protocols
- RFC 8098 - Message Disposition Notification
- RFC 8314 - Use of Transport Layer Security (TLS) for Email Submission and Access
A to jsou jen aktuálně platné verze těch záležitostí, bez kterých se prakticky neobejdete. Připočtu-li méně používané protokoly, zastaralé verze standardů a méně zajímavé dodatky ke specifických funkcím u těch aktuálních, bude jich nejméně 20x tolik. Kapitolou samou pro sebe jsou pak překlady doménových názvů, povolené znaky a formáty e-mailových adres, které by bez problémů vydaly na samostatný článek (třeba takový o validaci doménových jmen regulárním výrazem). Samozřejmě nečekám, že každý, kdo chce poslat e-mail, bude všechny znát, ale je pokud programuji aplikaci, která s elektronickou poštou nějakým způsobem pracuje, bylo by dobré mít alespoň ponětí o tom, že nějaké standardy na tohle téma existují a ve chvíli, kdy to nejméně čekám, mohou vyskočit a kousnout mě do zadku.
Šunku nebo lančmít?
Oukej, takže teď už víme, že mail by měl nějak vypadat, aby vyhověl standardům. To je docela důležité i v boji se nevyžádanou poštou. Průměrný antispam většinou kouká na obsah zprávy a pokud vypadá jako spam, tak zprávu zahodí, označí nebo prostě provede jinou nakonfigurovanou akci. Opravdu dobrý antispam by ale zprávu, která vypadá jako spam, měl bez problémů propustit. To je docela divné tvrzení, viďte? Tak já jej upřesním. Opravdu dobrý antispam by měl propustit zprávu, která vypadá jako spam, ale ve skutečnosti spamem není.
Přestavte si situaci, kdy vám do firemního mailu přijde nějaká nevyžádaná reklama na levný a zajisté kvalitní medicínský produkt. A vy ten mail vezmete a přepošlete jej svému IT administrátorovi s upozorněním, že vám přišel spam, ať s tím něco udělá. Je to úplně ten samý mail, ale v prvním případě je nevyžádaný a nechcete, aby chodil, kdežto v druhém už je cílený a naopak chcete, aby zamýšlenému adresátovi dorazil. Ano, já vím, s opravdu kvalitním antispamem by se to nestalo a uživatelská identifikace toho co je a není spam se taky v ideálním případě dělá jinak, ale pracujme dál v této hypotetické rovině. Jak má tedy antispam poznat, která z těch dvou stejných zpráv je legitimní a která ne? No, právě z nějakých těch ostatních dat, která nejsou přímo v obsahu zprávy. Takže koukne třeba, odkud byl odeslán a porovná adresu s blacklisty nebo reputačními systémy. Zkontroluje SPF a DKIM záznamy, pokud existují. Koukne do různých Bayes filtrů a distribuovaných databází se vzorky, zda mail došel opakovaně na více adres. Koukne, zda nebyl podvržen odesílatel. A v neposlední řadě také koukne na hlavičky mailu a jejich validitu a konformitu. Tedy zjednodušeně řečeno – Chcete-li, aby vaše pošta nachytala co nejmenší spam skóre, musíte se ujistit, že odpovídá standardům a je odesílána z důvěryhodného serveru. Důvěryhodnost serveru jakožto aplikační vývojář většinou neovlivníte, takže se pojďme podívat, jaké náležitosti musí e-mail mít, aby měl co největší šanci na doručení.
Pozn.: Když v následujících bodech říkám musí, je povinný a tak podobně, tak tím myslím, že je taková konvence dána standardem. Samozřejmě se nemusí dodržet nic, ale pak taková zpráva taky nemusí přijít.
- Musí obsahovat hlavičky From, To, Subject, Message-ID a Date. Dále by měla obsahovat hlavičky MIME-Version a Content-Type a v závislosti na typu obsahu pak ještě případně další, ale o tom později. Zajímá-li vás, jaké další hlavičky můžete do mailu nacpat a jak mají vypadat, RFC 4021 a RFC 5322 jsou výborným čtivem.
- Hlavičky From a To musí být ve formátu
"Zobrazené jméno" <adresa@example.com>
. Přičemž zobrazovaná jména jsou volitelná, ale špičaté závorky kolem odesílatele a adresáta jsou povinné. Je-li příjemců více, oddělují se středníkem. Stejná pravidla platí i pro hlavičky Cc, Reply-To a podobné. - Hlavičky From a To nesmí podvrhovat cizí mailové adresy. Tento bod si často aplikační vývojáři neuvědomují. Máte-li kontaktní formulář, do kterého uživatel vyplňuje svou mailovou adresu, pak tato adresa nesmí být použíta v hlavičce From, protože vaše aplikace takovým doménovým jménem nedisponuje. Praktika podvrhování adres je snad nejspamovitějším znakem vůbec. Pak se velmi snadno stane, že maily s podrženými adresami nikdy nedorazí, protože si doručující server zkontroluje SPF záznam k dané doméně, uvidí, že odesílající server nemá oprávnění k odesílání mailů s tímto doménovým názvem a mail okamžitě zahodí.
- Hlavička Message-ID musí být ve formátu
<nejake-id@example.com>
. Samotné ID může být jakýkoliv alfanumerický řetězec. Tečky, pomlčky a pár dalších paznaků jsou také povoleny. Doménová část nemusí obsahovat žádné existující doménové jméno, ale je dobrým zvykem použít název serveru nebo domény odkud mail odchází. Pokud není Message-ID explicitně zadáno aplikací, je u většiny systémů doplněno odesílajícím poštovním serverem. To samozřejmě platí pouze za předpokladu, že se aplikace nepřipojuje přímo na server adresáta. U PHP aplikací doporučuji Message-ID nechat na odesílajícím serveru a zabývat se jím pouze pokud to vyžadují interní mechanismy aplikace nebo pokud uvidíte, že odchozí zprávy žádné MID nemají. - Hlavička Date musí být ve formátu vyhovujícím RFC 5322, resp. RFC 2822. Vypadá třeba takto:
Tue, 21 Aug 2018 17:09:14 +0200
. PHP má na takový formát naštěstí přímo jediné zástupné písmenko, takže datum vyhovující standardu vypadne z prostéhodate('r')
. Stejně jako v případě Message-ID jej odesílající server většinou přidá sám, takže není nutno datum řešit explicitně. - Veškeré hlavičky by měly být zakódovány sedmibitovým kódováním. Tohle je trochu ošemetná záležitost, takže o ní se rozepíšu o kousek níže.
- Tělo mailu by mělo být taktéž zakódováno sedmibitovým kódováním a navíc v závislosti na Content-Type může obsahovat více sekcí. V případě, že odesíláte HTML zprávu, měli byste ji odesílat jako typ multipart/alternative a měla by mít i plaintextovou část. To u některých aplikací může být obtížně implementovatelné, obzvlášť v PHP, které k tomu nemá žádné nativní funkce. Spam skóre to většinou kazí jen nepatrně, takže za to hlavy netrhám. Nicméně i o multipart zprávách se zmíním níže.
- A na závěr ještě drobnost. Hlavičky musí být ve formátu
Klíč-dvojtečka-mezera-hodnota
a jednotlivé hlavičky musí být odděleny pomocí CRLF. Tedy v řeči PHP"\r\n"
. Většina systémů je sice dost chytrá, aby pochopila i samotné LF, ale standard předepisuje CRLF. Anebo prostě hlavičky mailovací funkci nacpěte jako asociativní pole a ona už si to přebere.
Pitevní zpráva
Když jsme si vyjasnili, co od zprávy očekáváme, pojďme se tedy podívat, co z PHP vypadne, použijeme-li v úvodu zavrhovanou funkci mail(). Zkusíme odeslat jednoduchý e-mail na adresu recipient@example.com s předmětem „Testovací zpráva“ a tělem zprávy „Testovací zpráva pro ověření funkce mailového systému.“.
<?php
$recipient = 'recipient@example.com';
$subject = 'Testovací zpráva';
$body = 'Testovací zpráva pro ověření funkce mailového systému.';
mail($recipient, $subject, $body);
Výsledná zpráva mi přišla takto:
Return-Path: <www-data@example.com>
Received: by example.com (Postfix, from userid 33)
id 595CA521F07; Tue, 21 Aug 2018 12:52:57 +0200 (CEST)
To: recipient@example.com
Subject: Testovacà zpráva
Message-Id: <20180821105257.595CA521F07@example.com>
Date: Tue, 21 Aug 2018 12:52:57 +0200 (CEST)
From: www-data <www-data@example.com>
X-Spam: Yes
TestovacĂ zprĂĄva pro ovÄ>Ĺ™enĂ funkce mailovĂŠho systĂŠmu.
V prvé řadě vidím, že to nějak nerozdýchalo kódování. Taky vidím, že mailový server, kterému PHP předalo zprávu k odeslání, automaticky doplnil Message-ID, Date a nějaké další méně zajímavé hlavičky sloužící pouze k identifikaci trasy mailu. Také mi sám doplnil hlavičku From, kterou nastavil na hodnotu systémového účtu, pod kterým na serveru PHP provozuji. Nakonec na posledním řádku vidím, že mi e-mail antispam označil jako spam. To právě proto, že tak hrubě nevyhovuje standardům. Naštěstí mám hodného administrátora serveru, který mi řekne, jaké příznaky zpráva nasbírala a proč. Antispam tvrdí, že zpráva má mimo jiné následující nedostatky:
- Předmět nemá správné kódování.
- Stejně tak není deklarována žádná znaková sada v hlavičce Content-Type nebo Charset a to i přesto, že kódování očividně není sedmibitové.
- Chybějcí Content-Type a Mime-Version navíc také neumožňuje spolehlivě určit, co je obsahem zprávy. Zde je to text, HTML, RTF nebo jak to má klient vlastně zobrazit.
Já ještě navíc vidím, že mnou předaná hlavička To není obalena špičatými závorkami, protože jsem si je tam sám nenapsal. To je naštěstí jen malý prohřešek. Zbytek hlaviček už je celkem OK, ale zpráva je tak doprasená, že to stačí k tomu, aby ji antispam označil a doručil rovnou do složky s nevyžádanou poštou. Kdybych ještě navíc odesílal z nějakého pochybně nastaveného serveru, klidně bych mohl nasbírat tolik bodů, aby antispam zprávu zahodil úplně.
Písmenková polívka
Takže to musíme nějak opravit. Buď se na můžeme na všechno vykašlat a sáhnout po nějaké hotové knihovně typu PHPMailer, která mimo to, že spoustu problémů vyřeší za nás, umožňuje odesílat maily přímo na server příjemce i bez lokálního odesílajícího agenta. Ale to zdaleka není taková zábava a nic byste se nedozvěděli, takže varianta druhá je použít ty správné kouzelné funkce a těch pár řádků trošku vyšperkovat. K tomu nám nejvíce pomůže PHP modul mbstring. Existuje od PHP 4, a pokud jej na svém serveru nebo hostingu náhodou nemáte, tak si jej doinstaluje nebo hodně hlasitě vydupejte, protože na češtinu a jiné jazyky s obskurními krucánky u písmenek je to nedocenitelná pomůcka. Stejně tak zastane kus práce i v případě, že potřebujete v PHP nějakým způsobem přežvýkávat emoji 🙈 🙉 🙊. Těmi kouzelnými funkcemi, kterými si usnadníte život jsou mb_send_mail(), kterážto je přímou náhradou funkce mail() s tím rozdílem, že si u předmětu a těla zprávy umí vyřešit kódování a správně jej pak indikovat i v hlavičkách. Druhou funkcí, kterou budete nejspíš potřebovat, je mb_encode_mimeheader() který zakóduje zbytek hlaviček, které do mb_send_mail()
předáte.
No a jak to s tím kódováním vlastně je? Celou dobu tu melu o nějakém sedmibitovém. SMTP, milé děti, umí tři různé druhy kódování. To základní, které existuje od pradávna, je sedmibitové prostě proto, že umí obsáhnout pouze spodní polovinu ASCII tabulky, tedy US-ASCII rozsah (hex 00-7F). V počítačovém pravěku, kdy se o Unicode nikomu ani nezdálo, bylo celkem obvyklé, že ASCII tabulka byla rozdělena na spodní (hex 00-7F) a horní (hex 80 - FF) polovinu, přičemž ta spodní byla pro drtivou většinu kódování stejná a ta horní se lišila v závislosti na kódové stránce daného regionu nebo jazyka. Kdo si pamatuje mode con codepage select=852
v autoexec.bat v DOSu, tak to je přesně ono. No, a aby se mohl uživatel používající jednou kódovou stránkou bavit s uživatelem používajícím jinou, posílali si pouze ty společné znaky z dolní poloviny ASCII tabulky, které se vlezly do sedmibitového prostoru. O hodně později se SMTP naučilo 8BITMIME, čili kódování osmibitové. Začalo být tedy možné v mailech posílat diakritiku, resp. znaky z horní poloviny ASCII tabulky. Tím pádem bylo nutno i říct jakým kódováním dotyčný mluví, k čemuž se stejně jako u HTTP použije hlavička Content-Type, případně Charset. Bystrému čtenáři tedy nejspíš dojde, že absence této informace je důvodem k rozhození kódování, kterého jsme byli svědky v příkladu výše. Bez této informace záleží pouze na poštovním klientovi, které kódování se pokusí použít jako první. Teoreticky by nám tedy pomohlo deklarovat Content-Type, ale to by problém řešilo pouze v případě poštovních serverů neimplementujících ten třetí způsob kódování.
Tím třetím je SMTPUTF8. UTF-8 je kódování s proměnlivou délkou. Jeden znak může zabírat od jednoho do čtyř bajů, resp. kvůli způsobu, jakým je indikována délku znaku, efektivně mezi 7 a 21 bity. SMTPUTF8 umožňuje úplnou internacionalizaci a podporuje divokou směsku znaků od naší latinky, přes azbuku a alfabetu až po různé čínské, japonské, korejské, indické a všelijaké další znakové sady a to nejen v těle zprávy a adresách odesílatele a příjemce, ale i v hodnotách ostatních hlaviček. Problémem ale je, že se jedná o poměrně nový standard (RFC 6532, rok 2012), a proto ještě zdaleka není implementován všemi mailovými servery. V době psaní toho článku jej implementuje Postfix, Sendmail, Exim a možná pár dalších méně významných SMTP serverů. Z velkých poskytovatelů mailu jej umí Google a Microsoft. V Česku je to bída, protože Seznam ani Centrum jej v současnosti neumí. Nicméně pokud SMTP serveru implementujícímu SMTPUTF8 nasypete do hlavičky UTF-8, tak o něm prostě ví a snaží se použití UTF-8 vynutit, což se mu občas nepodaří a tak odesílání zprávy selže. Takže dokud nebude na světě krásně, v potocích nepoteče pivo, pečení holubi nebudou lítat do huby a poskytovatelé mailových služeb nebudou do puntíku dodržovat všechny standardy, je nutno naši krásnou češtinu a jiné ne-ASCII znaky kódovat ručně.
Sedm trpaslíků
Takže zpět na začátek. Sedmibitové kódování. V zásadě přichází v úvahu dvě metody, jak libovolná data nacpat do sedmibitového nebo ještě menšího prostoru. První metodou je Quoted-printable kódování. U něj se všechny znaky (až na pár výjimek), které jsou v US-ASCII, nechají tak, jak jsou, a ty zbylé se promění na jejich ordinální hexadecimální hodnoty (resp. hodnoty jejich bajtů) uvozenou rovnítkem. Tedy z řetězce „Testovací zpráva“ se stane Testovac=C3=AD=20zpr=C3=A1va
. A aby server věděl, že je použito zrovna takové kódování, celá paráda se obalí identifikátorem, takže vypadne =?UTF-8?Q?Testovac=C3=AD=20zpr=C3=A1va?=
. Podobným způsobem se escapují znaky i ve webových URL. Tam je místo znaku rovnítka znak procenta. Druhým často používaným způsobem je kódování base64. To už lidsky čitelné není ani omylem, protože se v něm jinak interpretují jednotlivé bitové skupiny. Princip je takový, že ze zprávy, kde jsou normálně znaky v osmibitových skupinách, se ukusuje po šesti bitech a hodnota této skupiny se použije jako index pro substituci z předem daného pole znaků ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
, kde, jak vidíte, jsou jen znaky z US-ASCII. Pokud v posledním kousku nevychází bity, vycpe se výsledný text rovnítky. Článek o base64 na Wikipedii to asi vysvětlí trochu lépe. Pro nás to znamená, že z řetězce „Testovací zpráva“ vypadne řetězec VGVzdG92YWPDrSB6cHLDoXZh
a i s obalením pro MIME hlavičku pak =?UTF-8?B?VGVzdG92YWPDrSB6cHLDoXZh?=
.
A přesně tohle pro nás udělají ony zmiňované funkce mb_encode_mimeheader()
a mb_send_mail()
. Ještě se sluší podoktnout, že SMTP standard požaduje kódování pouze u řetězců, které se jinak do 7bitového prostoru nevlezou. Tedy například hodnota hlavičky To: <recipient@example.com>
by tedy měla být odeslaná bez jakéhokoliv dalšího překódování. Přehnané kódování vám tedy také může vyhrát nějaké punkové bodíky navíc. Funkce mb_encode_mimeheader()
a mb_send_mail()
jsou naštěstí tak chytré, že si umí ohlídat i tohle.
Ještě jednou a pořádně
Začnu nejprve jenom tím, že vyměním mail()
za mb_send_mail()
.
<?php
$recipient = 'recipient@example.com';
$subject = 'Testovací zpráva';
$body = 'Testovací zpráva pro ověření funkce mailového systému.';
mb_send_mail($recipient, $subject, $body);
A světe div se, e-mail najednou vypadá takhle
Return-Path: <www-data@example.com>
Received: by example.com (Postfix, from userid 33)
id 7617A52043E; Tue, 21 Aug 2018 14:41:40 +0200 (CEST)
To: recipient@example.com
Subject: =?UTF-8?B?VGVzdG92YWPDrSB6cHLDoXZh?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: BASE64
Message-Id: <20180821124140.7617A52043E@example.com>
Date: Tue, 21 Aug 2018 14:41:40 +0200 (CEST)
From: www-data <www-data@example.com>
VGVzdG92YWPDrSB6cHLDoXZhIHBybyBvdsSbxZllbsOtIGZ1bmtjZSBtYWlsb3bDqWhvIHN5c3TD
qW11Lg==
Nejen že mi mb_send_mail()
krásně zakódoval předmět a tělo zprávy, ale ještě navíc mi jako mávnutím kouzelného proutku přidal všechny nezbytné hlavičky. Antispam je s takovým mailem plně spokojený a příznak o spamu tedy nepřidal. No a nebylo by to PHP, aby se jedna věc nedala dělat pěti způsoby, takže podobného chování vám můžou pomoci dosáhnout funkce quoted_printable_encode(), imap_8bit(), imap_binary(), iconv_mime_encode(), base64_encode() a možná ještě nějaké další. U všech ale čekejte nějaké pasti a nekonzistentní chování, ale na to jste v PHP už asi zvyklí.
Multipass
Tak si to trochu ztížíme. Přidáme nějaké hlavičky, přihodíme diakritiku i do nich a pošleme onen zmiňovaný multipart/alternative s plaintextem i HTML zároveň. Jak jsem již zmínil, PHP nemá žádné nativní funkce pro skládání těla mailu, takže je celou operaci nutno dělat tím nejprimitivnějším možným způsobem. Anebo opět použít nějakou třídu třetí strany, která to udělá za nás. Multipart funguje tak, že si uživatel zvolí nějaký řetězec, ať už náhodně nebo deterministicky, který se v těle mailu nebude jinak vyskytovat, aby nedošlo ke kolizi. Ten se deklaruje v hlavičce Content-Type jako boundary a v těle se pak prefixuje prázdným řádkem a dvěma pomlčkami. Za tím pak následují hlavičky deklarující kódování dané části, protože každá část může být kódována jinak. Na konci zprávy se boundary do těla vrzne ještě jednou a tentokrát se za něj místo hlaviček přidají dvě další pomlčky, aby bylo jasné, že tady mail končí.
Narážíme tu ale na obrovský problém. Funkce mb_send_mail()
totiž očekává, že to, co se jí předá jako tělo, je první a poslední část zprávy, kterou má zakódovat celou od prvního do posledního znaku. My ale máme částí víc, a protože můžou mít různá kódování, musíme si je poskládat a zakódovat sami. Obrovským obloukem se tak dostávám na samotný začátek článku. V tomto případě totiž opravdu budeme muset použít holou funkci mail()
, ale zúročíme veškeré dosud načerpané znalosti a připravíme a předáme jí vše, co je to potřeba k tomu, aby byla odeslaná zpráva v souladu se standardy.
<?php
$sender = mb_encode_mimeheader('Nejlepší firma').' <sender@example.cz>';
$recipient = mb_encode_mimeheader('Můj milý uživatel').' <recipient@example.com>';
$subject = mb_encode_mimeheader('Testovací zpráva', 'UTF-8', 'Q');
$boundary = 'XXX';
$headers = [
'From' => $sender,
'Reply-To' => $sender,
'MIME-Version' => '1.0',
'Content-Type' => 'multipart/alternative; boundary="'.$boundary.'"; charset=UTF-8'
];
$body_plain = 'Testovací zpráva pro ověření funkce mailového systému.';
$body_html = '<!DOCTYPE html><html><body><p>'.$body_plain.'</p></body></html>';
$body = [
'--'.$boundary,
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: quoted-printable',
'',
quoted_printable_encode($body_plain),
'',
'--'.$boundary,
'Content-Type: text/html; charset=UTF-8',
'Content-Transfer-Encoding: BASE64',
'',
imap_binary($body_html),
'',
'--'.$boundary.'--'
];
$body = implode("\r\n", $body);
mail($recipient, $subject, $body, $headers);
Příchozí mail pak bude vypadat takto
Return-Path: <www-data@example.com>
Received: by example.com (Postfix, from userid 33)
id 75224527596; Tue, 21 Aug 2018 22:36:14 +0200 (CEST)
To: =?UTF-8?B?TcWvaiBtaWzDvSB1xb5pdmF0ZWw=?= <recipient@example.com>
Subject: =?UTF-8?Q?Testovac=C3=AD=20zpr=C3=A1va?=
From: =?UTF-8?B?TmVqbGVwxaHDrSBmaXJtYQ==?= <sender@example.com>
Reply-To: =?UTF-8?B?TmVqbGVwxaHDrSBmaXJtYQ==?= <sender@example.com>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="XXX"; charset=UTF-8
Message-Id: <20180821203614.75224527596@example.com>
Date: Tue, 21 Aug 2018 22:36:14 +0200 (CEST)
--XXX
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Testovac=C3=AD zpr=C3=A1va pro ov=C4=9B=C5=99en=C3=AD funkce mailov=C3=
=A9ho syst=C3=A9mu.
--XXX
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: BASE64
PCFET0NUWVBFIGh0bWw+PGh0bWw+PGJvZHk+PHA+VGVzdG92YWPDrSB6cHLD
oXZhIHBybyBvdsSbxZllbsOtIGZ1bmtjZSBtYWlsb3bDqWhvIHN5c3TDqW11
LjwvcD48L2JvZHk+PC9odG1sPg==
--XXX--
Kombinovat v jednom mailu Quoted-printable a base64 samozřejmě není ideální a i taková věc může rozsvítit nějakou méně důležitou antispamovou kontrolku. Já je v příkladu kombinuji pouze abych ilustroval všelijaké možné způsoby. Pokud si můžete vybrat, doporučuji sáhnout spíše po Quoted-printable. Je starší, takže by s ním neměl mít problémy ani žádný prehistorický systém. Sluší se ještě podotknout, že Quoted-printable i base64 předepisují maximální délku řádku včetně zalomení na 76 znaků. Nedodržení tohoto požadavku opět může vyústit ve výhružně vztyčený prst antispamového systému. Mnou použité funkce quoted_printable_encode()
a imap_binary()
zalamují řádky automaticky. Jiné, například base64_encode()
, to nedělají.
TL;DR
Mám-li onen lán textu shrnout do několika málo vět, pak v závislosti na potřebách aplikace a programátorově entusiasmu doporučuji používat následující kombinace:
- Nepoužívám multipart maily a chci se co nejmíň nadřít – Použijte
mb_encode_mimeheader()
a nahraďtemail()
zamb_send_mail()
. - Používám multipart maily, ale chci mít plnou kontrolu nad tím, co posílám. Nechci používat skripty třetích stran – Použijte funkci
mail()
, hlavičky sanitizujte pomocímb_encode_mimeheader($x, 'UTF-8, 'Q')
a těla pomocíquoted_printable_encode()
. Zkontrolujte si, zda posíláte všechny důležité hlavičky. - Používám multipart maily a nevadí mi skripty třetích stran – Použijte PHPMailer.
Jak se to dělá jinde
A na závěr malá ochutnávka toho, jak se maily posílají v méně zdraví škodlivých jazycích. Třeba můj oblíbený python posílá multipart maily takto
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formatdate
msg = MIMEMultipart('alternative')
msg['Subject'] = 'Testovací zpráva'
msg['From'] = '{} <sender@example.com>'.format(Header('Nejlepší firma').encode())
msg['To'] = '{} <recipient@example.com>'.format(Header('Můj milý uživatel').encode())
body_plain = 'Testovací zpráva pro ověření funkce mailového systému.';
body_html = '<!DOCTYPE html><html><body><p>{}</p></body></html>'.format(body_plain);
msg.attach(MIMEText(body_plain, 'plain', 'UTF-8'))
msg.attach(MIMEText(body_html, 'html', 'UTF-8'))
Ošetření hlaviček a obsahu není nijak vázáno na odesílací funkci a nějaké veletoče s boundary tu nejsou potřeba. Všechno se děje pod pokličkou. V tuto chvíli máte prostě hotový mail a můžete si s ním dělat, co chcete. Balík email je součástí standardní knihovny a jeho dokumentace explicitně zmiňuje, že záměrně neimplementuje žádné možnosti odesílání, protože k tomu tu jsou jiné balíky. Pokud jej chcete poslat stejným způsobem, jako to dělá PHP, pak můžete pustit třeba
from subprocess import run
run(['/usr/sbin/sendmail', '-t'], input=msg.as_bytes())
A šmytec.