Tag: email

Rails3, SMTP i różne konta (adresy) e-mail

Wysyłanie e-maili za pomocą Railsów jest bardzo proste. Zarówno przez sendmail jak i po smtp. Wystarczy w pliku config/production.rb (czy tez w innym - zależnie od środowiska), dodać następujące linijki:

  config.action_mailer.delivery_method = :smtp
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = true

  config.action_mailer.smtp_settings = {
    :address => "adres.serwera.smtp.pl",
    :port => 587,
    :domain => 'jakasmoja.domena.pl',
    :user_name => '<username>',
    :password => '<password>',
    :authentication => 'plain',
    :enable_starttls_auto => true
  }

i już nasza aplikacja Rails wysyła e-maile za pośrednictwem zewnętrznego serwera SMTP.

Problem pojawia się jeśli chcielibyśmy korzystać z różnych adresów e-mail, niekoniecznie na jednym serwerze SMTP. Takie przypisane na "sztywno" w configu ustawienia, pozwalają korzystać tylko z jednego serwera.

W starszych wersjach Railsów (1.x i 2.x) była sobie w mailerze zmienna @@smtp_settings która odpowiadała za przechowywanie ustawień związanych z serwerem SMTP. Nadpisanie tej zmiennej, powodowało automatyczne wysyłanie wiadomości z nowego (innego) serwera. W nowych Railsach ten mechanizm przestał działać. Zamiast zmiennej klasowej, jest teraz metoda self.smtp_settings w której przechowywane są ustawienia.

Aby sprawnie zarządzać kontami z których wysyłam maile, napisałem pewien niewielki kawałek kodu prezentowany poniżej. Ustawienia kont przechowywane są w plikach yaml, dzięki czemu łatwo można je edytować i zmieniać. Zanim zacznę, nadmienię tylko że konwencja nazewnicza w app/mailers nie do końca jest poprawna. Wynika ona z pewnych konwencji przyjętych przeze mnie jeszcze za czasów Rails2, gdzie Mailery znajdowały się w app/models. Nie ma to jednak żadnego wpływu na działanie kodu - piszę to tylko dlatego, że niektórzy mogliby się dziwić dlaczego mam namespace Mailer w app/mailers, skoro samo umiejscowienie w app/mailers, wskazuje na jego zawartość.

Proszę się nie czepiać że jest Mailer::Base i Mailer::Admin ;)

Wracając do sedna sprawy, chcielibyśmy wysyłać maile za pomocą różnych kont. Pierwszą rzeczą jaką musimy zrobić, jest powiadomienie Railsów o tym jak chcemy wysyłać maile w danym środowisku. Robimy to dodając w pliku config/environments/srodowisko.rb następujące linijki:

  config.action_mailer.delivery_method = :smtp
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = true

Mówią one o tym że chcemy wysyłać e-maile z pomocą SMTP, chcemy aby były wysyłane oraz chcemy być informowani jeśli z jakiegoś powodu e-mail nie zostanie wysłany. Kolejną rzeczą jaką zrobimy, będzie utworzenie katalogu config/mailers, w którym będziemy trzymać ustawienia związane z kontami.

W tym katalogu utwórzmy sobie plik yaml o nazwie admin. Dlaczego admin? Na potrzeby tutoriala będą dwie klasy:

  1. Mailer::Base
  2. Mailer::Admin

Pierwsza to klasa bazowa, z niej nie będziemy wysyłać e-maili. Drugą będzie wyżej wspomniana klasa Mailer::Admin. Właśnie stąd admin. Gdybyśmy mieli np Mailer::Newsletter, utworzylibyśmy plik newsletter.yml.

Plik ten ma następującą strukturę:

development:
  default:
    domain: "domena.pl"
    user_name: "mail@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true

production:
  default:
    domain: "domena.pl"
    user_name: "mail@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true

test:
  default:
    domain: "localhost"
    user_name: "login"
    password: "haselko"
    address: "localhost"
    port: 587
    authentication: plain

Plik taki zawiera deklarację domyślnych danych dostępowych dla każdego ze środowisk. Póki co nie zawiera nic więcej.

Zajmijmy się teraz naszą klasą bazową Mailer::Base:

# coding: utf-8
class Mailer::Base < ActionMailer::Base
  default :content_type => "text/html"
  # Czy załadowalismy sami ustawienia czy też zaraz przed wysłaniem ma
  # Załadować domyślne ustawienia
  @settings_loaded = false

  def mail(headers={}, &block)
    # Załaduj najpierw
    load_settings unless @settings_loaded
    super
  end

  private

  # Ładuje ustawienia specyficzne dla danego maila (skąd ma go wysyłać)
  # Ustawienia ładuje z yamla w config/mailer/
  def load_settings(mail = 'default')
    @settings_loaded = true
    # Wezmy nazwe tego modelu (bo z koncowej nazwy bedziemy brali parametr
    # dot. nazwy pliku w configach mailerów
    # Np. Mailer::Admin ==> admin.yml
    file_name = self.class.name.downcase.split('::').last
    # Wczytaj ustawienia
    options = YAML.load_file("#{Rails.root}/config/mailers/#{file_name}.yml")[Rails.env][mail]
    # Jeśli sie nie udało załadować takich ustawień to załaduj domyślne
    unless options
      options = YAML.load_file("#{Rails.root}/config/mailers/#{file_name}.yml")[Rails.env]['default']
    end
    # I załaduj je jako wlaściwe do wysłania maili
    options.each{|key, val|
      self.smtp_settings[key.to_sym] = val
    }
  end
end

Jak widać powyżej, kodu nie ma dużo. Pierwszą ważną rzeczą jest zmienna instancyjna @settings_loaded, która mówi nam, czy ustawienia dotyczące SMTP zostały już załadowane. Dzięki temu możemy rozpoznać, czy mamy ładować ustawienia domyślne, czy też załadowane zostały ustawienia specyficzne dla danego mailera. Dalej mamy przedefiniowaną metodę mail. Dodaliśmy tam tak naprawdę tylko jedną linijkę:

load_settings unless @settings_loaded

która załaduje nam domyślne ustawienia mailingowe, tylko jeśli ustawienia specyficzne nie zostały załadowane wcześniej.

Dalej mamy deklarację prywatnej metody load_settings, która przyjmuje jako parametr nazwę klucza z yamla, pod którym znajdują się parametry konta SMTP.

Przykładowo mając konto o kluczu "supermail", ładowalibyśmy jego ustawienia w taki sposób:

load_settings('supermail')

Metoda ta stara się odczytać ustawienia z podanego klucza i jeśli się to nie uda to ładuje ustawienia domyślne.

Ustawienia ładowane są tutaj:

    options.each{|key, val|
      self.smtp_settings[key.to_sym] = val
    }

Tyle jeśli chodzi o klasę bazową dla naszych mailerów. Utwórzmy sobie teraz klasę Mailer::Admin, która będzie wysyłała maila z informacją o zmianie adresu e-mail.

# coding: utf-8
class Mailer::Admin < Mailer::Base
  default :from => "info@przyklad.pl"

  def email_confirm(user)
    load_settings('email_confirm')
    @title = "Potwierdzenie zmiany adresu e-mail użytkownika #{user.login.capitalize}"
    @login = user.login.capitalize
    @msg = 'Twój adres e-mail został zmieniony'
    mail(:to => user.email, :subject => @title)
  end

end

Dziedziczymy po naszym mailerze bazowym, podając domyślną wartość pola from. Ładujemy następnie ustawienia konta, specyficzne dla e-maili email_confirm. Jeśli pominęlibyśmy ładowanie ustawień, wtedy e-maile byłyby wysyłane z domyślnego konta jakie mamy skonfigurowane w pliku admin.yml.

I to by było na tyle. Nie zapomnijcie dodawać ustawień do plików yml, jeśli nie wysyłacie z konta domyślnego. Poniżej przykład pliku admin.yml, wraz z ustawieniami domyślnymi oraz specyficznymi dla email_confirm:

development:
  default:
    domain: "domena.pl"
    user_name: "mail@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true
  email_confirm:
    domain: "domena.pl"
    user_name: "confirmation@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true

production:
  default:
    domain: "domena.pl"
    user_name: "mail@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true
  email_confirm:
    domain: "domena.pl"
    user_name: "confirmation@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true

test:
  default:
    domain: "localhost"
    user_name: "login"
    password: "haselko"
    address: "localhost"
    port: 587
    authentication: plain
  email_confirm:
    domain: "domena.pl"
    user_name: "confirmation@domena.pl"
    password: "haselko"
    address: "adres.pl"
    port: 587
    authentication: plain
    enable_starttls_auto: true

Rozsyłanie poczty e-mail wraz z osadzonymi obrazkami (base64)

Rozsyłając e-maile, niejednokrotnie chcielibyśmy je uatrakcyjnić. Sam suchy tekst nie jest ani ładny, ani zbyt wymowny. Najprostszym sposobem na uatrakcyjnienie naszych wiadomości e-mail, jest zastosowanie w nim zamiast suchego tekstu (text/plain) - HTMLa (text/html). Niesie to za sobą jednak kilka problemów:

  • Nie wszystkie readery obsługują HTML (aczkolwiek te nieobsługujące to raczej wyjątki)
  • Nie wszystkie readery obsługują CSS i osadzanie obrazów (np. google reader - aczkolwiek nie zawsze ;) )
  • Część readerów obsługuje tylko część znaczników i selectorów

Jak więc sprawić żeby nasze e-maile były ładne tam gdzie się da oraz czytelne tam gdzie trzeba?

Nie jest to wbrew pozorom aż tak trudna sprawa. Pierwszą rzeczą jaką należy przygotować jest tekst. W naszym przypadku posłużymy się tekstem który już mam. Dotyczy on zmiany adresu e-mail w Susanoo.

Cała wiadomość wygląda tak:

Szanowny Użytkowniku,

Niniejszy e-mail wysłany został ze względu na rozpoczętą procedurę zmiany adresu e-mail konta: XYZ. Jeżeli nie wykonywałeś takiej dyspozycji - zignoruj tę wiadomość.

Aby dokończyć operację, potwierdź ją: potwierdzam
Możesz także skorzystać z poniższego odnośnika, kopiując go do swojej przeglądarki internetowej:

http://localhost:3000/admin/core/account/confirm/jakistamhash

Jeśli nie aktywujesz nowego adresu e-mail w przeciągu 24 godzin, system automatycznie przywróci Twój poprzedni adres e-mail.

Oczywiście tutaj jako cytat wygląda beznadziejnie. Sprawimy więc żeby jako e-mail wyglądała tak jak trzeba. Nie opiszę tutaj całego procesu tworzenia takiego "ładnego" e-maila. Opiszę jednak metodologię abyście sami mogli rozwijać własne wzory e-maili.

Zanim zaczniemy, wyznaczmy sobie cele. Chcemy aby w readerze który jest dość słaby, wiadomość wyglądała tak:

zrzut_ekranu

Jak widać, jest nagłówek, zwrot grzecznościowy oraz treść. Na końcu i na początku są też notki dot. tego że wiadomość rozsyłał automat. Jest przejrzyście i prosto. Ale z drugiej strony, w readerze obsługującym osadzanie styli oraz obrazki w  base64, ta wiadomość wygląda tak:

zrzut_ekranuww

Jak widać, wiadomość prezentuje się całkiem nieźle. Jak osiągnąć taki podwójny efekt? Przede wszystkim należy pamiętać że Gmail nie wspiera ani osadzania CSSa w plikach zewnętrznych, ani też w headerze strony. Jedynym wyjściem są proste style inline. I tak właśnie postąpiłem w jego wypadku. Skorzystałem z prostych styli jak:

  • color
  • padding
  • margin
  • text-decoration
  • font-size

Aby nadać mu podstawowy wygląd. Jednocześnie skorzystałem z pewnej sztuczki. Aby nasze style które osadzimy dla "fajniejszych" readerów, nie gryzły się z tymi dla słabych, należało tak utworzyć CSS aby nie kolidował jeden z drugim. Było to stosunkowo proste. O ile w przypadku Gmaila styl inline wyglądał tak:

<div id="confirm_contener" style="margin-top: 20px; margin-bottom: 20px; padding: 20px 10px;">

O tyle już CSS "normalny" był taki:

padding-bottom:  20px;

Jak widać jest to podobny kod, jednak inline ustawia marginesy tylk górny i dolny, zaś nasz "CSSowy" ustawia je wszystkie. W ten sposób uzyskujemy wcięcie z każdej strony. Większość pracy właśnie polega na utworzeniu niekolidujących ze sobą styli, co tak naprawdę nie jest trudne o ile najpierw tworzymy styl zaawansowany który umieszczamy w headerze strony jako:

<style type="text/css">
    jakieś style ...
</style>

Dzięki temu wyświetlanie w klientach jak Gmail nastąpi bez uwzględnienia tych styli. Dla takich readerów wstawiamy CSS inline.

Pozostaje jeszcze kwestia grafik. Tutaj są dwie możliwości:

  1. Grafiki zdalne - doczytywane z naszego serwera
  2. Grafiki inline - zakodowane w samej wiadomości

Ja skłoniłem się ku rozwiązaniu drugiemu. Dlaczego? Otóż wiele klientów webmail (jak i część desktopowych) domyślnie nie ładuje grafik zewnętrznych traktując je jako potencjalnie groźną zawartość zdalną. Pojawia się monit z pytaniem czy przeglądarka ma wyświetlić zawartość zdalną. Nie bardzo mi się to podobało i postanowiłem oszczędzić użytkownikom zbędnych pytań.

Osadzam więc te newielkie graficzki jak tło czy przycisk, jako obrazy inline osadzone w ciele dokumentu. Jak tego dokonać?

Mając obrazek, przykładowo obrazek tła, wchodzimy tutaj:

http://www.motobit.com/util/base64-decoder-encoder.asp

I ładujemy tam nasz plik. Serwis ten umożliwia przekonwertowanie dowolnego pliku na jego odpowiednik zakodowany w base64, dzięki czemu możemy go wstawić do kodu inline. Należy przy tym pamiętać ze konwerter ten, dodaje znaki nowego wiersza, więc trzeba je pousuwać (cały "tekst"| wynikowy musi być w jednej linii). Nasz plik po przekonwertowaniu, stanie się "czytelny" i możliwy do wklejenia. Będzie wyglądał mniej więcej tak (tylko będzie tego trochę więcej):

WC1BY2NvdW50LUtleTogYWNjb3VudDQNClgtVUlETDogR21haWxJZDEyYjQ1Z
WJlYWEwZWNiZWQNClgtTW96aWxsYS1TdGF0dXM6IDAwMDENClgtTW96aW
xsYS1TdGF0dXMyOiAwMDAwMDAwMA0KWC1Nb3ppbGxhLUtleXM6ICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
CAgICAgICAgICAgICAgICAgICAgICAgICAgDQpEZWxpdmVyZWQtVG86IG1lbn
Nm

Taki "tekst" wstawiamy do kodu. Jeśli chodzi o wstawianie np. tła w CSS, wykonujemy to w taki sposób:

background: url('data:image/gif;base64,R0lGODlhBQAFAJEAABgYGC4uLigoK=');

Jak więc widzicie wystarczy dodać: data:image/gif;base64, a po tym wkleić naszą zwartość. Od tego momentu w wypadku zaawansowanych czytników, będziemy mieli ładne tło dla danego elementu.

Jeśli chodzi zaś o osadzanie elementów klikalnych jak img, sytuacja nie różni się zbytnio od poprzedniej. W przypadku HTMLa wygląda to tak:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhE">

Wpisujemy w src data:image/png;base64,, po czym naszą zawartość, zamykamy i już. Jest jednak w tym rozwiązaniu jeden minus. Mianowicie jeśli mamy elementy klikalne, które są podlinkowane, musimy dostarczyć alternatywę dla prostych readerów. Tą alternatywą jest alt. Sam obrazek nie będzie widoczny, jednak zamiast niego będzie tekst który osadzimy właśnie w alt. Dodatkowo dodamy jeszcze prosty styl inline aby obrazki nie miały obramowania. Całość będzie wyglądać następująco:

    <a href="www.dev.mensfeld.pl" id="img_confirm">
        <img src="data:image/png;base64,iVBORw0..." alt=" potwierdzam" style="border:0; margin:0; padding: 0"/>
    </a>

To by było na tyle. Dzięki odpowiedniemu ustawieniu stylów w headerze, stylów inline oraz osadzeniu obrazów wraz z atrybutem alt możemy uzyskać fajnie wyglądające wiadomości e-mail, tam gdzie się da, oraz funkcjonalne i przejrzyste tam gdzie wyświetla się tylko tekst oraz ew. podstawowe style inline.

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑