Trvale udržitelné mailování z webových aplikací

Odeslat e-mail z ASP.NET je snadné. Pokud máte aplikaci, která generuje větší množství různých typů mailů, a chcete je nějak rozumně spravovat, lokalizovat do jiných jazyků a podobně, úkol už není zdaleka tak jednoduchý. Ve svých aplikacích používám třídu, která řeší všechny problémy, které jsem až doposud řešil.

Čeho chceme dosáhnout

Jaké problémy řešíme v případě automaticky generovaných e-mailů?

Lokalizace. Pokud je rozhraní aplikace v několika jazycích, měla by zde být možnost automaticky posílat lokalizované zprávy. A to například včetně jména a e-mailové adresy odesílatele.

Konzistence. Zprávy z jednoho webu by měly mít nějakou "štábní kulturu", aby uživatel snadno poznal, čeho se daný web týká. U automaticky generovaných mailů je také pravděpodobnější, že budou omylem odchyceny antispamovým filtrem. Je tedy dobrý nápad uživateli jejich přijímání usnadnit, aby si mohl například nastavit nějaká vhodná pravidla pro whitelisting. Například všechny zprávy z tohoto webu mají na začátku subjectu text [www.aspnet.cz].

Nezaškodí také, aby maily věly nějaké jednotné zápatí (signaturu), kdežtě napíšete takové informace jako který web to odeslal, jak kontaktovat provozovatele a možná i populární větičku "Tato zpráva byla odeslána automaticky, neodpovídejte na ni."

Trvalá udržitelnost. Texty předmětných zpráv je třeba čas od času měnit. Někdy musejí být modifikovatelné uživatelsky (v tom případě je třeba je vecpat do databáze), jindy může změnu provést programátor. Ale i v tomto případě je vhodné, aby nemusel editovat C# kód, kde se nepřehledně skládá text mailu.

Personalizace. Do stejné šablony potřebujeme doplnit - a odpovídajícím způsobem formátovat - měnící se data.

Řešení

Moje řešení využívá resource soubory v ASP.NET - RESX. Resources obecně slouží k ukládání různých typů dat. Do resource lze uložit stringy, obrázky, ikony, zvuky... K těmto pak lze programově přistupovat několika způsoby.

Resources se používají ve světě ASP.NET převážně pro lokalizaci. Přepínáním vlastnosti CurrentUiCulture aktuálního threadu můžeme volit, jaká jazyková verze se použije.

Přístup k resource souborům je rychlý a pohodlný. V konečném důsledku se distribuují ve formě přídavných DLL souborů, ve zkompilované formě.

Použití RESX souborů tedy z aktualizací nevyviňuje programátora -- pouze mu poněkud zjednodušuje práci, protože má všechny zprávy na jednom místě.

Vytvoření RESX souboru a práce s ním

Resource soubory v prostředí .NET jsou XML soubory, které jsou zpracovávány při kompilaci. Je možno k nim přistupovat strongly-typed způsobem a nebo pomocí Resource Manageru.

Resource soubory musejí být uloženy v adresáři ~/App_GlobalResources/. Vytvořte si jej ve své aplikaci a přidejte do něj ve Visual Studiu položku typu Resource File a nazvěte ji MailMessages.resx.

Poklepáním otevřete vizuální editor, což je grid se třemi sloupci: Name, Value a Comment. Name je klíč, pod nímž později bude daná hodnota dostupná. Value je vlastní hodnota. Comment je komentář, který se programově nevyužívá, ale může sloužit jako pomůcka pro překladatele při překládání do cizího jazyka. Můžete v něm obecně uvést třeba konkrétní kontext slova, aby překladatel věděl, zda má "volume" překládat jako "hlasitost" nebo jako "svazek" a nedopadlo to jako ve Windows. Vytvořte si novou položku, pojmenujte ji MailSenderName a zadejte do ní nějakou hodnotu, která se použije jako jméno odesílatele e-mailů.

Nyní můžete k této hodnotě programově přistupovat tak, že napíšete Resources.MailMessages.MailSenderName. .NET automaticky vytvoří proxy třídu a vygeneruje vlastnosti tak, aby odpovídaly položkám resource souboru.

Druhá možnost je, že budete k hodnotám přistupovat pomocí resource manageru a jeho metody GetString takto: string s = Resources.MailMessages.ResourceManager.GetString("MailSenderName").

V našem případě budeme využívat obě dvě tyto varianty. Pro přístup k pevně definovaným položkám použijeme strongly-typed metodu, pro přístup k obecným šablonám metodu GetString.

Základní nastavení

Nejprve v resource souboru definujeme několik obecných položek pro všechny zprávy:

  • MailSenderName - zobrazované jméno odesílatele, např. ASPNET.CZ
  • MailSenderEmail - e-mailová adresa odesílateke, např. [email protected].
  • MailEncodingName - název kódování, v němž bude e-mail odeslán, např. iso-8859-2. V zásadě bychom mohli tuto položku pominout a všechny zprávy posílat v UTF-8. To by ale znamenalo zbytečný nárůst jejich objemu, protože by se kódovaly pomocí Base64. Použitím single-byte kódování umožníme použití v daném případě efektivnější metody Quoted Printable.
  • MailSubjectFormatString - formátovací řetězec, který se použije pro vytvoření předmětu zprávy. Na místo, kam má být vložen subject, umístěte {0}. Například: [www.aspnet.cz] {0}.
  • MailBodyFormatString - formátovací řetězec pro text zprávy, vytvořený dle výše uvedeného vzoru. Pro vložení zalomení řádku použijte klávesovou kombinaci Shift+Enter.

Poté definujeme statickou třídu Mailer a v ní následující metody:

public static void SendMail(string recipient, string subject, string body) {

    // Validate arguments

    if (recipient == null) throw new ArgumentNullException("recipient");

    if (string.IsNullOrEmpty(recipient)) throw new ArgumentException("Value cannot be null or empty string.", "recipient");

    SendMail(new string[] { recipient }, subject, body);

}

 

public static void SendMail(string[] recipients, string subject, string body) {

    // Validate arguments

    if (recipients == null) throw new ArgumentNullException("recipients");

    if (subject == null) throw new ArgumentNullException("subject");

    if (body == null) throw new ArgumentNullException("body");

    if (string.IsNullOrEmpty(subject)) throw new ArgumentException("Value cannot be null or empty string.", "subject");

    if (string.IsNullOrEmpty(body)) throw new ArgumentException("Value cannot be null or empty string.", "body");

  pro

    using (MailMessage msg = new MailMessage()) {

        // Prepare message

        msg.From = new MailAddress(Resources.MailMessages.MailSenderEmail, Resources.MailMessages.MailSenderName);

        msg.Subject = string.Format(Resources.MailMessages.MailSubjectFormatString, subject);

        msg.Body = string.Format(Resources.MailMessages.MailBodyFormatString, body).Replace("\\n", "\r\n");

        msg.SubjectEncoding = System.Text.Encoding.GetEncoding(Resources.MailMessages.MailEncodingName);

        msg.BodyEncoding = System.Text.Encoding.GetEncoding(Resources.MailMessages.MailEncodingName);

 

        // Send messages

        SmtpClient mx = new SmtpClient();

        foreach (string recipient in recipients) {

            if (string.IsNullOrEmpty(recipient)) continue;

            msg.To.Clear();

            msg.To.Add(recipient);

            mx.Send(msg);

        }

    }

}

Nyní máme k dispozici metody pro obecné odeslání e-mailu s dodržením výše definovaých parametrů, a to v overloadech ro zaslání jednomu nebo více příjemcům.

Šablony mailů

Dále je záhodno vyřešit ještě vlastní text mailů. I k tomu využijeme resources. Pro každý typ mailu budeme mít dvě položky, jednu pro subject a jednu pro text, jmenovat se budou NázevŠablonySubject a NázevŠablonyBody.

Předmět zprávy je v mé implementaci statický, text zprávy opět může obsahovat zástupky ve formátu {číslo}, které budou potom použity při formátování výsledného řetězce.

K odeslání šablonovaného mailu slouží metoda SendTemplatedMail:

public static void SendTemplatedMail(string recipient, string templateName, params object[] args) {

    // Validate arguments

    if (recipient == null) throw new ArgumentNullException("recipient");

    if (string.IsNullOrEmpty(recipient)) throw new ArgumentException("Value cannot be null or empty string.", "recipient");

    SendTemplatedMail(new string[] { recipient }, templateName, args);

}

 

public static void SendTemplatedMail(string[] recipients, string templateName, params object[] args) {

    // Validate arguments

    if (recipients == null) throw new ArgumentNullException("recipients");

    if (templateName == null) throw new ArgumentNullException("templateName");

    if (string.IsNullOrEmpty(templateName)) throw new ArgumentException("Value cannot be null or empty string.", "templateName");

 

    // Get subject and body of message

    string subject = Resources.MailMessages.ResourceManager.GetString(templateName + "Subject");

    string body = Resources.MailMessages.ResourceManager.GetString(templateName + "Body");

    subject = string.Format(subject, args);

    body = string.Format(body, args);

 

    // Send messages

    SendMail(recipients, subject, body);

}

I tato metoda má dva overloady, pro jedoho a pro více příjemců. Kromě adresáta vyžaduje ještě název šablony, který se potom použije pro určení odpovídajícího názvu resource položky. Dále pak obsahuje pole parametrů args, kam se předají argumenty, které metoda string.Format použije na místa zástupek {0}, {1} atd.

Vlastní odeslání mailu tak lze provést příkladně takto:

Mailer.SendTemplatedMail("[email protected]", "ApproveRequest",

    (int)user.ProviderUserKey,     // 0

    r.Nick,                        // 1

    r.Species,                     // 2

    r.Email,                       // 3

    r.FullName,                    // 4

    r.Citizenship,                 // 5

    r.Role                         // 6

    r.UiCulture,                   // 7

    Altairis.Web.Utils.BaseUrl()); // 8

Lokalizace

Pokud budeme chtít texty lokalizovat, stačí vytvořit kopii souboru MailMessages.resx, která bude před příponou obsahovat ISO kód jazyka, ve kterém texty jsou. Pro angličtinu se tedy soubor bude jmenovat MailMessages.en.resx. Pak přeložíme obsah souboru do požadovaného jazyka.

RESX soubory jsou normální XML, pro jejich překlad tedy stačí běžný textový editor, překladatel nemusí mít Visual Studio. Pohodlnější ale bude použít k překladu specializovaný program. Doporučuji například RESX Editor, který je k dispozici zdarma.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET