Why can't the Mail block see my variable?

2019-03-27 06:31发布

问题:

I'm new to Ruby and wondering why I am getting an error in this situation using the 'mail' gem in a simple Sinatra app:

post "/email/send" do

  @recipient = params[:email]

  Mail.deliver do 
    to @recipient # throws error as this is undefined
    from 'server@domain.com'
    subject 'testing sendmail'
    body 'testing sendmail'
  end

  erb :email_sent

end

This however works fine:

post "/email/send" do

  Mail.deliver do 
    to 'me@domain.com'
    from 'server@domain.com'
    subject 'testing sendmail'
    body 'testing sendmail'
  end

  erb :email_sent

end

I suspect this is something to do with block scope and my misunderstanding of it.

回答1:

As Julik says, Mail#delivery executes your block using #instance_exec, which simply changes self while running a block (you wouldn't be able to call methods #to and #from inside the block otherwise).

What you really can do here is to use a fact that blocks are closures. Which means that it "remembers" all the local variables around it.

recipient = params[:email]
Mail.deliver do 
    to recipient # 'recipient' is a local variable, not a method, not an instance variable
...
end

Again, briefly:

  • instance variables and method calls depend on self
  • #instance_exec changes the self;
  • local variables don't depend on self and are remembered by blocks because blocks are closures.


回答2:

If you'll read through the docs for Mail further you'll find a nice alternate solution that will work. Rather than use:

Mail.deliver do 
  to @recipient # throws error as this is undefined
  from 'server@domain.com'
  subject 'testing sendmail'
  body 'testing sendmail'
end

you can use Mail's new() method, passing in parameters, and ignore the block:

Mail.new(
  to:      @recipient,
  from:    'server@domain.com',
  subject: 'testing sendmail',
  body:    'testing sendmail'
).deliver!

or the alternate hash element definitions:

Mail.new(
  :to      => @recipient,
  :from    => 'server@domain.com',
  :subject => 'testing sendmail',
  :body    => 'testing sendmail'
).deliver!

In pry, or irb you'd see:

pry(main)> Mail.new(
pry(main)* to: 'me@domain.com',
pry(main)* from: 'me@' << `hostname`.strip,
pry(main)* subject: 'test mail gem',
pry(main)* body: 'this is only a test'
pry(main)* ).deliver!
=> #<Mail::Message:59273220, Multipart: false, Headers: <Date: Fri, 28 Oct 2011 09:01:14 -0700>, <From: me@myhost.domain.com>, <To: me@domain.com>, <Message-ID: <4eaad1cab65ce_579b2e8e6c42976d@myhost.domain.com>>, <Subject: test mail gem>, <Mime-Version: 1.0>, <Content-Type: text/plain>, <Content-Transfer-Encoding: 7bit>>

The new method has several variations you can use. This is from the docs also, and might work better:

As a side note, you can also create a new email through creating a Mail::Message object directly and then passing in values via string, symbol or direct method calls. See Mail::Message for more information.

 mail = Mail.new
 mail.to = 'mikel@test.lindsaar.net'
 mail[:from] = 'bob@test.lindsaar.net'
 mail['subject'] = 'This is an email'
 mail.body = 'This is the body'

followed by mail.deliver!.

Also note, in the previous example, that there are multiple ways to access the various headers in the message envelope. It's a flexible gem that seems to be well thought out and nicely follows the Ruby way.



回答3:

I think it's because the Mail gem uses instance_exec under the hood. instance_exec uses instance variables from the object it's being called on and not from the caller. What I'd do is find a method in the Mail gem that does not use instance tricks but passes an explicit configuration object to the block, and proceed from there. Spares a few gray hairs.



标签: ruby sinatra