Tag: sendmail

Padrino Mailer + undefined method “default_encoding”

Pracując nad mailingiem w pewnej aplikacji napisanej w Padrino natrafiłem na "głupi" błąd:

NoMethodError: undefined method `default_encoding' for Mail::Message:Class

Z takim oto stacktracem:

NoMethodError: undefined method `default_encoding' for Mail::Message:Class
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:466:in `render'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/rendering.rb:123:in `render'
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/ext.rb:228:in `render'
	/home/mencio/app/mailers/signup_mailer.rb:9
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/base.rb:57:in `instance_exec'
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/base.rb:57:in `email'
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/helpers.rb:82:in `call'
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/helpers.rb:82:in `deliver'
	/gems/padrino-mailer-0.9.19/lib/padrino-mailer/helpers.rb:34:in `deliver'
	/home/mencio/app/controllers/signups.rb:9:in `POST /signup'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:340:in `call'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:340:in `route'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:649:in `instance_eval'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:649:in `route_eval'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:605:in `route!'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:605:in `catch'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:605:in `route!'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:596:in `catch'
	/gems/padrino-core-0.9.19/lib/padrino-core/application/routing.rb:596:in `route!'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:766:in `dispatch!'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:559:in `call!'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:731:in `instance_eval'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:731:in `invoke'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:731:in `catch'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:731:in `invoke'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:559:in `call!'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:544:in `call'
	/gems/rack-flash-0.1.1/lib/rack/flash.rb:148:in `call'
	/gems/padrino-core-0.9.19/lib/padrino-core/reloader.rb:31:in `call'
	/gems/padrino-core-0.9.19/lib/padrino-core/logger.rb:282:in `call'
	/gems/warden-1.0.3/lib/warden/manager.rb:35:in `call'
	/gems/warden-1.0.3/lib/warden/manager.rb:34:in `catch'
	/gems/warden-1.0.3/lib/warden/manager.rb:34:in `call'
	/gems/rack-less-1.5.0/lib/rack/less/base.rb:40:in `call!'
	/gems/rack-less-1.5.0/lib/rack/less/base.rb:24:in `call'
	/gems/rack-1.2.1/lib/rack/showexceptions.rb:24:in `call'
	/gems/rack-1.2.1/lib/rack/methodoverride.rb:24:in `call'
	/gems/rack-1.2.1/lib/rack/session/cookie.rb:37:in `call'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:1173:in `call'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:1199:in `synchronize'
	/gems/sinatra-1.1.3/lib/sinatra/base.rb:1173:in `call'
	/gems/padrino-core-0.9.19/lib/padrino-core/router.rb:71:in `call'
	/gems/padrino-core-0.9.19/lib/padrino-core/router.rb:64:in `each'
	/gems/padrino-core-0.9.19/lib/padrino-core/router.rb:64:in `call'
	/gems/rack-1.2.1/lib/rack/content_length.rb:13:in `call'
	/gems/rack-1.2.1/lib/rack/chunked.rb:15:in `call'
	/gems/thin-1.2.8/lib/thin/connection.rb:84:in `pre_process'
	/gems/thin-1.2.8/lib/thin/connection.rb:82:in `catch'
	/gems/thin-1.2.8/lib/thin/connection.rb:82:in `pre_process'
	/gems/thin-1.2.8/lib/thin/connection.rb:57:in `process'
	/gems/thin-1.2.8/lib/thin/connection.rb:42:in `receive_data'
	/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:in `run_machine'
	/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:in `run'
	/gems/thin-1.2.8/lib/thin/backends/base.rb:61:in `start'
	/gems/thin-1.2.8/lib/thin/server.rb:159:in `start'
	/gems/rack-1.2.1/lib/rack/handler/thin.rb:14:in `run'
	/gems/padrino-core-0.9.19/lib/padrino-core/server.rb:43:in `build'
	/gems/padrino-core-0.9.19/lib/padrino-core/server.rb:14:in `run!'
	/gems/padrino-core-0.9.19/lib/padrino-core/cli/adapter.rb:29:in `start'
	/gems/padrino-core-0.9.19/lib/padrino-core/cli/base.rb:23:in `start'
	/gems/thor-0.14.6/lib/thor/task.rb:22:in `send'
	/gems/thor-0.14.6/lib/thor/task.rb:22:in `run'
	/gems/thor-0.14.6/lib/thor/invocation.rb:118:in `invoke_task'
	/gems/thor-0.14.6/lib/thor.rb:263:in `dispatch'
	/gems/thor-0.14.6/lib/thor/base.rb:389:in `start'
	/gems/padrino-core-0.9.19/bin/padrino:8
	/gems/bin/padrino:19:in `load'
	/gems/bin/padrino:19

Niestety nie znalazłem żadnej wartościowej informacji na temat tego jak to naprawić, tak więc napisałem (brzydki ;) ) workaround który dopisujemy w app/app.rb:

module Mail
  class Message
    def self.default_encoding
    end
  end
end

Przynajmniej na "teraz" rozwiązanie to wydaje się działać.

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

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑