Category Archives: ASP.NET

mailto: Příprava mailů pro poštovního klienta včetně cc, bcc, subject a body

I při vývoji webových aplikací se občas setkáme s výstředním požadavkem, kdy je potřeba, aby aplikace „generovala maily pro Outlook“ (či jakéhokoliv poštovního klienta). Obchodníci prostě aplikacím nevěří, chtějí si mail před odesláním upravit, připojit vlastní podpis a mít ho ve vlastní Odeslané poště.

Obvykle chtějí „kliknu tady na to tlačítko a objeví se mi nový předvyplněný mail v Outlooku“… :-))
(Nebudu tu teď hodnotit, do jaké míry je to vhodné, spolehlivé, …)

Jednou z možných cest, jak výsledku dosáhnout, je použít HTML element

<a href="mailto:jmeno@prijmeni.cz">Tlačítko</a>

…asi nikoho nepřekvapuje, že takovýto odkaz vytvoří na klientu nový mail pro odeslání na zadanou adresu.

Ne každý však ví, že takovýto odkaz lze odekorovat tak, že mimo adresy příjemce předáme poštovnímu klientovi i Cc, Bcc, Subject, Body, či dokonce libovolnou hodnotu do hlavičky mailu. Není to kupodivu nic proprietarního, nýbrž to vyplývá z RFC 2368. Jednotlivé hlavičky najdete v RFC 822.

Můžeme tak například vytvořit bohatší odkaz:

<a href="mailto:infobot@example.com?subject=current-issue&amp;body=current-issue-body&amp;cc=jmeno@prijmeni.cz">Tlačítko</a>

Ještě jeden drobný zádrhel – jak nakódovat mezery, konce řádek, či dokonce češtinu. V zásadě bychom měli encodovat URL podle RFC 1738, nicméně v praxi to tak jednoduché není, protože např. v ASP.NET si se samotnými HttpUtility.UrlEncode(), ani HttpUtility.UrlPathEncode() nevystačíme:

  • mezerníky musíme převést na %20, nikoliv na + (to sice dělá UrlPathEncode(), ale ten zase neřeší konce řádek) ,
  • konce řádek musíme převést na %0D%0A, z praxe stačí i %0A (UrlEncode() prozměnu kóduje mezery na +),
  • …atp. i další speciální znaky.

Takže nakonec to může odkaz vypadat nějak takto:

<a href="mailto:?Subject=Nab%eddka%20%e8.%20&amp;Body=Pos%edl%e1m%20nab%eddku%0a%0aCENA%20%3d%201%a0114%20(111%2c40%2fks)">Tlačítko</a>

přičemž v ASP.NET lze odkaz emitovat třeba takto:

TlacitkoHL.NavigateUrl = String.Format(
    "mailto:{0}?Body={1}",
    email,
    HttpUtility.UrlEncode("tělo\nzprávy", Encoding.GetEncoding(1250)).Replace("+", "%20")
);

Ještě zajímavý postřeh – problémy s kódováním. Nezbývá než testovat, např. přestože samotná HTML stránka je třeba v kódování utf-8, je potřeba mailto URL odkaz encodovat do win-1250. Zřejmě je to o tom, že browser předá URL v nezměněné podobě poštovnímu klientovi (např. Outlooku) a informace o encodingu tam nikde mezi nimi neproběhne. Moc pěkné to není. Pokud byste k tomu někdo měl přesnější informace, uvítám je v komentáři. Mě to takhle funguje všude, kde to potřebuji, takže nemám teď motivaci ani čas nad tím dál bádat… ;-)

Refresh stránky po postbacku do počátečního stavu

Při ladění webové aplikace se často dostanete do situace, kdy vyzkoušítě nějaké postback funkce stránky, a pak se chcete vrátit v prohlížeči rychle zpět do původního výchozího stavu (IsPostBack = false), abyste vyzkoušeli něco jiného.

S prohlížečem může být s tímto docela boj, protože Back nás většinou hodí buď na předchozí adresu nebo vytáhne stránku z cache a GET neprovede, Refresh naopak znovu posílá POST data a zopakuje tak poslední postback. Dělá to prostě všechno možný, jen ne nový GET potřebné stránky.

Je na to jednoduchý trik, pokud chceme znovu načíst počáteční stav stránky, stačí změnit něco bezvýznamného v adrese stránky (adresním řádku prohlížeče). Oblíbeným indiferentním trikem je změna malého písmena na velké (pro prohlížeč je to jiná adresa, udělá požadovaný GET počátečního stavu, naopak IIS je case-insensitive, takže mu to nevadí).

http://www.northwind.com/my-page.aspX

…a je to.

Nested Repeaters – vnořování repeaterů

Vnořit Repeatery se může zdát potíž, dokud poprvé neuvidíte, jak je to jednoduché. Celý fígl totiž spočívá v data-bindingu vnitřních repeaterů v obsluze události ItemDataBound vnějšího Repeateru.

V příkladu vnější Repeater iteruje přes všechny obory činnosti (kategorie, skupiny) a vnitřní Repeater zobrazuje položky (zde „zápisy do katalogu“) příslušející danému oboru činnosti (kategorii, skupině).

MyPage.aspx

<asp:Repeater ID="OboryCinnostiRepeater" runat="server">
  <ItemTemplate>
     
   <%# ((OborCinnosti)Container.DataItem).Nazev %>
   
   <asp:Repeater ID="ZapisyRepeater" runat="server">
    <ItemTemplate>
      <%# ((ZapisDoKatalogu)Container.DataItem).Jmeno %>
     </ItemTemplate>
   </asp:Repeater>
  
  </ItemTemplate>
 </asp:Repeater>

MyPage.aspx.cs

private void OboryCinnostiRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
   RepeaterItem item = e.Item;
  
   // zajímají nás jen datové řádky, ne hlavička ani patička
   if ((item.ItemType == ListItemType.Item) || (item.ItemType == ListItemType.AlternatingItem))
   {
    // najdeme si vnitřní Repeater
    Repeater zapisyRepeater = (Repeater)item.FindControl("ZapisyRepeater");
  
    // a nabidnujeme mu data příslušející položce (oboru)
    OborCinnosti obor = (OborCinnosti)item.DataItem;
    ZapisDoKataloguCollection = obor.GetZapisy();
    
    zapisyRepeater.DataSource = zapisyOboru;
    zapisyRepeater.DataBind();
   }
}

File upload – HttpException (0x80004005): Request timed out.

Při uploadu velkých souborů (relativně dle rychlosti spojení) se můžeme setkat s chybou

HttpException (0x80004005): Request timed out.

Jde v podstatě o to, že IIS tlačí do ASP.NET data průběžně dlouhou dobu a request na straně ASP.NET vytimeoutuje. Stačí však nastavit parametr executionTimeout ve web.configu na dostatečnou hodnotu a je po problému:

<httpRuntime
     maxRequestLength="10240"
     executionTimeout="36000"
/>

…maxRequestLength je volba, na kterou se obvykle nezapomíná a která mění základní limit 4 MB (4096 KB) pro maximální velikost requestu (zadává se v KB, executionTimeout je v sekundách).

Download

Podle informací uživatele P.L. z newsgroupy microsoft.public.cs.developer vyřešil atributexecutionTimeout obdobný problém i s padáním dlouhotrvajících downloadů.

SessionPageStatePersister: Ukládání ViewState do Session

ViewState se standardně ukládá do formulářového hidden input-fieldu do stránky, posílá se tedy sem a tam na klienta (GET) a zpět na server (POST).

Díky mechanizmu page-adapterů a připraveného SessionPageStatePersister-u lze v ASP.NET velmi snadno přesměrovat ukládání ViewState do Session, tedy na stranu serveru. Primárně je tento mechanizmus určený pro použití se zařízeními (browsery), kde není možné nebo žádoucí view-state pomocí hidden-fieldu přenášet (PDA, mobily, atp.). Nic nám však nebrání rozšířit jeho využití na všechny stránky.

Potřebujeme pouze dvě věci:

1. Připravit page-adapter, který používá SessionPageStatePersister

Vysvětlovat mechanizmus control-adapterů je mimo rozsah tohoto článku – je to prostě něco, co dokáže poměrně dost modifikovat výchozí chování controlů (a stránka je control), například zajistit jiné renderování, nebo právě způsob ukládání ViewState.

Potřebný page-adapter bude vypadat takto:

public class PageAdapter : System.Web.UI.Adapters.PageAdapter
{
   public override PageStatePersister GetStatePersister()
   {
      return new SessionPageStatePersister(this.Page);
   }
}

…nejde o nic jiného, než říci, že se má použít připravený SessionPageStatePersister.

2. Aplikovat page-adapter pomocí .browser souboru

ASP.NET řekneme, že má příslušný page-adapter použít, tak, že modifikujeme/vytvoříme příslušný .browser soubor. Modifikovat lze buď globální nastavení ve složce <windir>\Microsoft.NET\Framework\<ver>\CONFIG\Browsers, nebo pro jednotlivou aplikaci vytvořit .browser soubor do složky ~/App_Browsers.

Soubor ~/App_Browsers/My.browser bude vypadat třeba takto:

<browsers>
   <browser refID="Default">
      <controlAdapters>
         <adapter controlType="System.Web.UI.Page" adapterType="MyNamespace.PageAdapter"/>
      </controlAdapters>
   </browser>
</browsers>

…to je prakticky vše, co musíme udělat pro ukládání ViewState do Session.

Alternativa – overrride Page.PageStatePersister

Pokud máme ve svém projektu zavedenu společnou bázovou třídu všech stránek (což každopádně i jinak doporučuji), pal můžeme místo PageAdapteru můžeme i rovnou overridování property PageStatePersister:

protected override PageStatePersister PageStatePersister
{
    get
    {
        if (_pageStatePersister == null)
        {
            _pageStatePersister = new SessionPageStatePersister(this.Page);
        }
        return _pageStatePersister;
    }
}
private PageStatePersister _pageStatePersister;

…nevýhodou je pevné zakomponování volby Session jako ViewState uložiště do aplikace, na rozdíl od předchozí konfigurační možnosti nad hotovou aplikací.

POZOR!!! Od property PageStatePersister se nedokumentovaně očekává, že ji bude během jednoho requestu možno volat opakovaně a stále bude vracet stejnou instanci! Zatímco metoda PageAdapter.GetStatePersister() je zvnějšku obalena cachováním instance, v property si tento mechanizmus musíme zajistit sami pomocí private fieldu.

Konfigurace SessionPageStatePersisteru

SessionPageStatePersister se dá konfigurovat z web.configu pomocí elementu <sessionPageState />:

<system.web>
  <sessionPageState historySize="9" />
</system.web>

Jediným nastavitelným atributem je historySize, kterým se volí počet ViewState záznamů, které má persister udržovat. Výchozí hodnota je 9.

Úskalí použití SessionPageStatePersisteru

  • ViewState je ukládán jako položky Session + existuje slovník, který udržuje jaké klíče v Session odpovídají jakému požadavku.
  • Výchozí velikost tohoto slovníku je 9 záznamů, lze však změnit pomocí konfigurace.
  • Každý požadavek vytvoří nový záznam, desátý požadavek vytěsní první.
  • Pokud si tedy uživatel otevře více než 9 oken, pak načtení view-state selhává!!! Metoda není tedy ve výchozím nastavení příliš vhodná pro stránky s frames, nebo různá dialogová okna.
  • Ztrátou Session ztratíme i ViewState, pokud tedy máme například InProc session a restartujeme webovou aplikaci, ViewState je pryč.

Východiskem z některých situací je detekce ztráty ViewState.

Podivné chování session, přegenerovávání SessionID

V aplikaci je přihlašovací dialog. Zde by měla aplikace vytvořit session a poslat její identifikátor klientovi, což se stane.

Po přihlášení se zobrazí stránka obsahující frames, každý frame (a iframe) však znovu dostává jiný identifikátor session. To je špatné, protože každému frame je potom poslána jiný identifikátor session a tudíž má jeden uživatel více session.

Problém nenastane, pokud do přihlašovacího dialogu umístím kód:

Session["some_key"] = "some_value";
Session.Clear();

Trochu mě překvapuje, že session Session.Clear() je možné provést a je možné mít tedy prázdnou session. ASP.NET tedy pravděpodobně ruší session, pokud s ní nebylo pracováno (nebylo k ní přistoupeno).

Celý problém jsem vyřešil potomkem HttpApplication (Global.asax):

private void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
    // tento kod opravuje podivne chovani Session.
    if (Context.Session != null)
    {
        Context.Session[&quot;__Application_PreRequestHandlerExecute&quot;] = &quot;some_value&quot;;
        Context.Session.Remove(&quot;__Application_PreRequestHandlerExecute&quot;);
    }
}

Několik ValidationGroup a jedno post-back tlačítko

Občas se nám může objevit potřeba, kdy potřebujeme formulář rozdělit na několik zón (ValidationGroup) a validaci v nich řídit samostatně, například podle toho, jako zónu si uživatel vybral radio-buttony.

ASP.NET bohužel ve verzi 1.1 nějaké podrobnější řízení validace nepodporuje, ve verzi 2.0 už dává alespoň validation groups (kterých využijeme), nicméně stále jejich použití váže na samostatnou inicializaci post-backu (pro každou validation group se předpokládá samostatné odesílací tlačítko).

Pokud potřebujeme formulář s jediným tlačítkem, který bude validovat na základě nějakého přepínače, který obsahuje (typicky na základě radio-buttonů nebo i drop-down-listu), pak nastávají potíže.

Validace na straně serveru

Pokud nám postačuje validace na straně serveru, je situace poměrně jednoduchá, protože využijeme metody Page.Validate(vaidationGroup) a následně ověřujeme už jenom Page.IsValid.

Validátory si rozdělíme na několik skupin

  1. ty, které chceme vyhodnocovat vždy, těm vlastnost ValidationGroup nenastavíme,
  2. ty, které chceme vyhodnocovat na základě přepínače, ty umístíme do příslušné skupiny ValidationGroup

Post-backovému tlačítku pak ValidationGroup nenastavíme, čímž dosáhneme toho, že i na klientu se nám budou vyhodnocovat alespoň ty společné validátory, které nemají nastaveno ValidationGroup.

V obsluze události na straně serveru pak postupujeme zhruba takto:

private void Tlacitko_Click(object sender, EventArgs e)
{
   // validátory bez ValidationGroup jsou vyhodnoceny
   // stačí se tedy ptát Page.IsValid
   if (IsValid)
   {
      if (PrepinacJednaRB.Checked)
      {
         // radio-buttonový přepínač je nastaven na zónu 1
         // vyvoláme si tedy vyhodnocení validátorů zóny 1
         Validate("ValidationGroupJedna");
         if (IsValid)
         {
            ...
            // obsluha zóny 1
         }
         else
         {
            return;
         }
      }
      else if (PrepinacDvaRB.Checked)
      {
         // přepínač je nastaven na zónu 2
         Validate("ValidationGroupDva");
         if (IsValid)
         {
            ...
            // obsluha zóny 2
         }
         else
         {
            return;
         }
      }
  
      // tady můžeme obsloužit controly společné pro všechny zóny
      // je splněna obecná validace i prošla validace zvolené zóny
      // takže třeba
      result.Save();
   }
}

Ještě je třeba doplnit, že i control ValidationSummary zobrazuje pouze hlášky příslúšné ValidationGroup, tedy bychom měly mít tolik ValidationSummary, kolik máme validation groups.

Validace na straně klienta

Požadujeme-li stejné chování validátorů už na straně klienta, je situace složitější. Musíme si totiž vytvořit klientský skript, který nejprve rozhodne na základě hodnot formuláře, která zóna je vybrána (který radio-button je zakliknut) a zavolá validace pro příslušné ValidationGroup.

Volání post-backu s příslušnou validací si vygenerujeme pomocí ClientScriptManager.GetPostBackEventReference(), kam přes PostBackOptions předámeValidationGroup. Horší to bude s vícenásobnou validací (např. společná + group), kde už si budeme muset pomoct nekonvenčními metodami a volat JScript ASP.NETu natvrdo.

Vzhledem k tomu, že v praxi pro takovéto klientské validace existují hotové a promyšlené validační controly třetích stran, nemá smysl se do této oblasti hlouběji potápět…

Připojuji také odkazy na několik článků, které popisují řízení validace na straně klienta:

Co by měl každy vědět o ViewState

1. ViewState není odpovědný za zachování vlastních hodnot controlů mezi postbacky

ViewState není potřeba pro zachování hodnot (Value) TextBoxů, CheckBoxů, DropDownListů a jiných Web controlů mezi postbacky. Tyto hodnoty jsou uloženy standardně ve formulářových postback datech (POST/GET) a ASP.NET je nastaví v metodě LoadPostData() pro všechny prvky, které implementují rozhraní IPostBackDataHandler. Na to, aby nám mezi roundtripy zůstal text v TextBoxu tedy nepotřebujeme ViewState!!!

2. Na co tedy ViewState?

ViewState je vlastnost každého controlu zděděná od System.Web.UI.Control (má ji tedy i Page) a jeho základní funkčnost je založena ná následující implementaci vlastností:

public string NavigateUrl
{
   get
   {
      string text = (string) ViewState["NavigateUrl"];
      if (text != null)
         return text;
      else
         return string.Empty;
   }
   set
   {
      ViewState["NavigateUrl"] = value;
   }
}

Hodnoty vlastností (property) se tedy ukládají do a čtou z ViewState, veškeré jejich změny se promítají do ViewState.

3. ViewState je typu StateBag

ViewState je typu System.Web.UI.StateBag, která implementuje mj. IDictionary (slouží k ukládání párů klíč-hodnota) a interně používá HybridDictionary.
Důležitou metodou StateBagu je SaveViewState(), která odpovídá za uložení ViewState.
Celý fígl je v tom, že metoda SaveViewState() uloží jenom ty vlastnosti, které se změnily po zavolání metody TrackViewState().

4. Co se tedy ukládá při uložení ViewState?

Funkčnost ViewState je úzce spojena s life-cyclem stránky a okamžikem volání metody TrackViewState(). Ta je volána na konci události Init každého controlu, a tedy i stránky (pro zjištění, zda-li již byla volána, lze použít property IsTrackingViewState).
Změny properties provedené před koncem události Init každého controlu se tedy s ViewState neukládají, veškeré další změny až do volání metodySaveViewState() (v události SaveViewState) ano.

5. Jak tedy stránka/control s ViewState funguje?

Vezměme si krátký příklad:

   private void btnSubmit_Click(object sender, EventArgs e)
   {
      lblMessage.Text = "Goodbye, Everyone!";
   }

Co se stane při první návštěvě stránky:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, není postback
  3. „Save ViewState stage“: nic se nestane, nejsou změny ViewState
  4. „Render Stage“: Label je renderován s „Hello, World!“

Co se stane při kliku na Change Message tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, ViewState stránky je prázdný
  3. „Raise Postback Event“: btnSubmit_Click nastaví
  4. lbl.Message=“Goodbye, Everyone!“
  5. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
  6. „Render Stage“: Label je renderován s „Goodbye, everyone!“

Co se stane při kliku na Empty postback tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nastaví se lblMessage.Text=“Goodbyw, Everyone!“ z ViewState
  3. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
    „Render Stage“: Label je renderován s „Goodbye, everyone!“

Shrnutí & spol.

  1. Do ViewState se ukládají všechny změny v properties controlů provedené po ukončení události Init.
  2. Protože ViewState ukládá pouze vlastnosti controlů a ne controly samotné, musíme dynamicky přidávané controly přidávat při každém postbacku stránky znovu a znovu – nejlépe během události Init (uděláme-li to však i později, metoda .Add() zajistí nahrání ViewState do přidávaných controlů).
  3. U editovatelných DataGridů je vypnutí ViewState docela dřina.
  4. ViewState se ukládá rekurzivně včetně ViewState child-controlů a to v serializované podobě pomocí LOSFormateru.
  5. ViewState lze ukládat i na serveru na disk nebo do databáze pomocí překrytí metod SavePageStateToPersistenceMedium() aLoadViewStateFromPersistenceMedium(), které standardně právě používají hidden-field ___VIEWSTATE. V některém z dalších článků si ukážeme ukládání do Session.
  6. Další možností na zmenšení ViewState je jeho komprese a dekomprese.
  7. ViewState je chráněn před změnami pomocí machine authentication check (MAC), který však pouze kontroluje, je-li ViewState od stejné verze stránky.
  8. ViewState lze i šifrovat, rozhodně se však nedoporučuje pro ukládání citlivých informací.

BUG: MasterPage zobrazuje default hodnotu ContentPlaceHolderu a ignoruje Content

Narazil jsem na zajímavý bug v ASP.NET 2.0 (2.0.50727)…

Pokud v ContentPlaceHolderu definujeme default obsah a použijeme v něm ebedded code block (starý vnořený ASP-style blok kódu <% %>, ale i <%= %>), pak ASP.NET ignoruje Content definovaný v konkrétních stránkách a stále zobrazuje pouze default obsah z MasterPage.

...
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
   <% %>   <-- už tohle vadí
   <%= "Tohle taky vadí" %>
   <%= ResolveUrl("~/takhle-na-to-asi-narazime/") %>
</asp:ContentPlaceHolder>
...

Zajímalo mě, kde je pravděpodobná příčina problému, takže jsem se díval, co z toho ASP.NET stvoří a jak se to liší od funkční podoby. Zásadní rozdíl je už v metodě __BuildControl, kde narozdíl od korektní podoby (ContentPlaceHolder1 s jedním Labelem):

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            return holder1;
      }
      IParserAccessor accessor1 = holder1;
      accessor1.AddParsedSubObject(new LiteralControl("\r\n\t\t\t"));
      Label label1 = this.__BuildControlTest();
      accessor1.AddParsedSubObject(label1);
      accessor1.AddParsedSubObject(new LiteralControl("\r\n        "));
      return holder1;
}

…chybí řádek return holder1;

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            // return holder1;  <-- pravděpodobně chybí
      }
      holder1.SetRenderMethodDelegate(new RenderMethod(this.__RenderContentPlaceHolder1));
      return holder1;
}
 
private void __RenderContentPlaceHolder1(HtmlTextWriter __w, Control parameterContainer)
{
      __w.Write("\r\n\t\t\t");
}

Jinak bug je již reportován v Microsoft Connect (Feedback center), můžete se připojit k hlasování…

V odesílaných mailech chybí háčky a čárky, někde samy mizí

Pozoruhodná situace může nastat, pokud explicitně nenastavíme encoding mailů odesílaných z ASP.NET aplikací (resp. obecně .NET aplikací).

Může se nám totiž stát, že aplikace, která na jednom serveru korektně odesílá maily, v pohodě s češtinou, po přesunu na server jiný (jinak nastavený), najednou posílá maily „napůl české“. Ono „napůl“ je na tom to nejzáludnější, zprávy totiž nechodí ve stylu rozsypaný čaj, jak to známe ze špatně nastaveného encodingu, nýbrž jsou ve zprávě odebrány háčky a čárky od většiny českých znaků (zůstává třeba á, í, é). Člověka tak hned nenapadne, v čem je problém a bádá někde mimo aplikaci.

…v mém konkrétním případě druhý server díky svému nastavení posílal zprávy v encodingu iso-8859-1 a sám (předpokládám CDO, ale nebádal jsem nad tím) si je upravoval na nejlépe čitelné.

Zásadně tedy vždy explicitně určovat encoding zpráv!!!

// v případě System.Web.Mail i System.Net.Mail
using (MailMessage mail = new MailMessage())
{
   mail.BodyEncoding = Encoding.GetEncoding("iso-8859-2");
   // System.Net.Mail.MailMessage má i SubjectEncoding
   ...
}