123456789_123456789_123456789_123456789_123456789_

Class: EventMachine::Protocols::SmtpClient

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Connection
Instance Chain:
Inherits: EventMachine::Connection
Defined in: lib/em/protocols/smtpclient.rb

Overview

Simple SMTP client

Sending generated emails (using Mail)

mail = Mail.new do from 'alice@example.com' to 'bob@example.com' subject 'This is a test email' body 'Hello, world!' end

email = EM::P::SmtpClient.send( :domain=>'example.com', :from=>mail.from.first, :to=>mail.to, :message=>mail.to_s )

Examples:

email = EM::Protocols::SmtpClient.send(
  :domain=>"example.com",
  :host=>'localhost',
  :port=>25, # optional, defaults 25
  :starttls=>true, # use ssl
  :from=>"sender@example.com",
  :to=> ["to_1@example.com", "to_2@example.com"],
  :header=> {"Subject" => "This is a subject line"},
  :body=> "This is the body of the email"
)
email.callback{
  puts 'Email sent!'
}
email.errback{ |e|
  puts 'Email failed!'
}

Constant Summary

Deferrable - Included

Pool

LineText2 - Included

MaxBinaryLength

Class Method Summary

Connection - Inherited

.new

Override .new so subclasses don't have to call super and can ignore connection-specific arguments.

Instance Attribute Summary

Connection - Inherited

#comm_inactivity_timeout

comm_inactivity_timeout returns the current value (float in seconds) of the inactivity-timeout property of network-connection and datagram-socket objects.

#comm_inactivity_timeout=

Allows you to set the inactivity-timeout property for a network connection or datagram socket.

#error?

Returns true if the connection is in an error state, false otherwise.

#notify_readable=

Watches connection for readability.

#notify_readable?,
#notify_writable=

Watches connection for writeability.

#notify_writable?

Returns true if the connection is being watched for writability.

#paused?,
#pending_connect_timeout

The duration after which a TCP connection in the connecting state will fail.

#pending_connect_timeout=

Sets the duration after which a TCP connection in a connecting state will fail.

#signature, #watch_only?

Instance Method Summary

LineText2 - Included

#receive_binary_data

S

#receive_data,
#receive_end_of_binary_data

S

#receive_line

S

#set_binary_mode

Alias for #set_text_mode, added for back-compatibility with LineAndTextProtocol.

#set_delimiter

The line delimiter may be a regular expression or a string.

#set_line_mode

Called internally but also exposed to user code, for the case in which processing of binary data creates a need to transition back to line mode.

#set_text_mode,
#unbind

In case of a dropped connection, we'll send a partial buffer to user code when in sized text mode.

Deferrable - Included

#callback

Specify a block to be executed if and when the Deferrable object receives a status of :succeeded.

#cancel_callback

Cancels an outstanding callback to &block if any.

#cancel_errback

Cancels an outstanding errback to &block if any.

#cancel_timeout

Cancels an outstanding timeout if any.

#errback

Specify a block to be executed if and when the Deferrable object receives a status of :failed.

#fail

Sugar for set_deferred_status(:failed, ...).

#set_deferred_failure

Alias for Deferrable#fail.

#set_deferred_status

Sets the "disposition" (status) of the Deferrable object.

#set_deferred_success
#succeed

Sugar for set_deferred_status(:succeeded, ...).

#timeout

Setting a timeout on a Deferrable causes it to go into the failed state after the Timeout expires (passing no arguments to the object's errbacks).

Connection - Inherited

#associate_callback_target

conn_associate_callback_target.

#close_connection

EventMachine::Connection#close_connection is called only by user code, and never by the event loop.

#close_connection_after_writing
#connection_completed

Called by the event loop when a remote TCP connection attempt completes successfully.

#detach

Removes given connection from the event loop.

#disable_keepalive

t_disable_keepalive.

#enable_keepalive

t_enable_keepalive.

#get_cipher_bits, #get_cipher_name, #get_cipher_protocol,
#get_idle_time

The number of seconds since the last send/receive activity on this connection.

#get_outbound_data_size

conn_get_outbound_data_size.

#get_peer_cert

If TLS is active on the connection, returns the remote X509 certificate as a string, in the popular PEM format.

#get_peername

This method is used with stream-connections to obtain the identity of the remotely-connected peer.

#get_pid

Returns the PID (kernel process identifier) of a subprocess associated with this Connection object.

#get_proxied_bytes

The number of bytes proxied to another connection.

#get_sni_hostname, #get_sock_opt,
#get_sockname

Used with stream-connections to obtain the identity of the local side of the connection.

#get_status

Returns a subprocess exit status.

#initialize

Stubbed initialize so legacy superclasses can safely call super.

#original_method,
#pause

Pause a connection so that EventMachine#send_data and #receive_data events are not fired until #resume is called.

#post_init

Called by the event loop immediately after the network connection has been established, and before resumption of the network loop.

#proxy_completed

called when the reactor finished proxying all of the requested bytes.

#proxy_incoming_to

EventMachine::Connection#proxy_incoming_to is called only by user code.

#proxy_target_unbound

Called by the reactor after attempting to relay incoming data to a descriptor (set as a proxy target descriptor with EventMachine.enable_proxy) that has already been closed.

#receive_data

Called by the event loop whenever data has been received by the network connection.

#reconnect

Reconnect to a given host/port with the current instance.

#resume

Resume a connection's EventMachine#send_data and #receive_data events.

#send_data

Call this method to send data to the remote end of the network connection.

#send_datagram

Sends UDP messages.

#send_file_data

Like Connection#send_data, this sends data to the remote end of the network connection.

#set_sock_opt,
#ssl_handshake_completed

Called by ::EventMachine when the SSL/TLS handshake has been completed, as a result of calling #start_tls to initiate SSL/TLS on the connection.

#ssl_verify_peer

Called by ::EventMachine when :verify_peer => true has been passed to EventMachine#start_tls.

#start_tls

Call EventMachine#start_tls at any point to initiate TLS encryption on connected streams.

#stop_proxying

A helper method for EventMachine.disable_proxy

#stream_file_data

Open a file on the filesystem and send it to the remote peer.

#unbind

called by the framework whenever a connection (either a server or client connection) is closed.

Constructor Details

.newSmtpClient

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 71

def initialize
  @succeeded = nil
  @responder = nil
  @code = nil
  @msg = nil
end

Class Method Details

.send(args = {})

:host => required String a string containing the IP address or host name of the SMTP server to connect to. :port => optional defaults to 25. :domain => required String This is passed as the argument to the EHLO command. :starttls => optional Boolean If it evaluates true, then the client will initiate STARTTLS with the server, and abort the connection if the negotiation doesn't succeed. TODO, need to be able to pass certificate parameters with this option. :auth => optional Hash of auth parameters If not given, then no auth will be attempted. (In that case, the connection will be aborted if the server requires auth.) Specify the hash value :type to determine the auth type, along with additional parameters depending on the type. Currently only :type => :plain is supported. Pass additional parameters :username (String), and :password (either a String or a Proc that will be called at auth-time).

@example :auth => :username=>"mickey@disney.com", :password=>"mouse"

:from => required String Specifies the sender of the message. Will be passed as the argument to the MAIL FROM. Do NOT enclose the argument in angle-bracket (<>) characters. The connection will abort if the server rejects the value. :to => required String or Array of Strings The recipient(s) of the message. Do NOT enclose any of the values in angle-brackets (<>) characters. It's NOT a fatal error if one or more recipients are rejected by the server. (Of course, if ALL of them are, the server will most likely trigger an error when we try to send data.) An array of codes containing the status of each requested recipient is available after the call completes. TODO, we should define an overridable stub that will be called on rejection of a recipient or a sender, giving user code the chance to try again or abort the connection.

One of either :message, :content, or :header and :body is required:

:message => String A valid RFC2822 Internet Message. :content => String Raw data which MUST be in correct SMTP body format, with escaped leading dots and a trailing dot line. :header => String or Hash of values to be transmitted in the header of the message. The hash keys are the names of the headers (do NOT append a trailing colon), and the values are strings containing the header values. TODO, support Arrays of header values, which would cause us to send that specific header line more than once.

@example :header => => "Bogus", "CC" => "myboss@example.com"

:body => Optional String or Array of Strings, defaults blank. This will be passed as the body of the email message. TODO, this needs to be significantly beefed up. As currently written, this requires the caller to properly format the input into CRLF-delimited lines of 7-bit characters in the standard SMTP transmission format. We need to be able to automatically convert binary data, and add correct line-breaks to text data.

:verbose => Optional. If true, will cause a lot of information (including the server-side of the conversation) to be dumped to $>.

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 138

def self.send args={}
  args[:port] ||= 25
  args[:body] ||= ""

=begin
(I don't think it's possible for EM#connect to throw an exception under normal
circumstances, so this original code is stubbed out. A connect-failure will result
in the #unbind method being called without calling #connection_completed.)
begin
  EventMachine.connect( args[:host], args[:port], self) {|c|
    # According to the EM docs, we will get here AFTER post_init is called.
    c.args = args
    c.set_comm_inactivity_timeout 60
  }
rescue
  # We'll get here on a connect error. This code mimics the effect
  # of a call to invoke_internal_error. Would be great to DRY this up.
  # (Actually, it may be that we never get here, if EM#connect catches
  # its errors internally.)
  d = EM::DefaultDeferrable.new
  d.set_deferred_status(:failed, {:error=>[:connect, 500, "unable to connect to server"]})
  d
end
=end
  EventMachine.connect( args[:host], args[:port], self) {|c|
    # According to the EM docs, we will get here AFTER post_init is called.
    c.args = args
    c.set_comm_inactivity_timeout 60
  }
end

Instance Attribute Details

#args=(value) (writeonly)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 169

attr_writer :args

Instance Method Details

#connection_completed

This method is for internal use only.
[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 178

def connection_completed
  @responder = :receive_signon
  @msg = []
end

#escape_leading_dots(s) (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 335

def escape_leading_dots(s)
  s.gsub(/^\./, '..')
end

#invoke_auth (private)

Perform an authentication. If the caller didn't request one, then fall through to the mail-from state.

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 282

def invoke_auth
  if @args[:auth]
    if @args[:auth][:type] == :plain
      psw = @args[:auth][:password]
      if psw.respond_to?(:call)
        psw = psw.call
      end
      #str = Base64::encode64("\0#{@args[:auth][:username]}\0#{psw}").chomp
      str = ["\0#{@args[:auth][:username]}\0#{psw}"].pack("m").gsub(/\n/, '')
      send_data "AUTH PLAIN #{str}\r\n"
      @responder = :receive_auth_response
    else
      return invoke_internal_error("unsupported auth type")
    end
  else
    invoke_mail_from
  end
end

#invoke_data (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 339

def invoke_data
  send_data "DATA\r\n"
  @responder = :receive_data_response
end

#invoke_ehlo_over_tls (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 271

def invoke_ehlo_over_tls
  send_ehlo
  @responder = :receive_ehlo_over_tls_response
end

#invoke_error (private)

We encountered an error from the server and will close the connection. Use the error and message the server returned.

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 217

def invoke_error
  @return_values.elapsed_time = Time.now - @return_values.start_time
  @return_values.responder = @responder
  @return_values.code = @code
  @return_values.message = @msg
  set_deferred_status :failed, @return_values
  send_data "QUIT\r\n"
  close_connection_after_writing
end

#invoke_internal_error(msg = "???") (private)

We encountered an error on our side of the protocol and will close the connection. Use an extra-protocol error code (900) and use the message from the caller.

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 230

def invoke_internal_error msg = "???"
  @return_values.elapsed_time = Time.now - @return_values.start_time
  @return_values.responder = @responder
  @return_values.code = 900
  @return_values.message = msg
  set_deferred_status :failed, @return_values
  send_data "QUIT\r\n"
  close_connection_after_writing
end

#invoke_mail_from (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 305

def invoke_mail_from
  send_data "MAIL FROM: <#{@args[:from]}>\r\n"
  @responder = :receive_mail_from_response
end

#invoke_rcpt_to (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 314

def invoke_rcpt_to
  @rcpt_responses ||= []
  l = @rcpt_responses.length
  to = @args[:to].is_a?(Array) ? @args[:to] : [@args[:to].to_s]
  if l < to.length
    send_data "RCPT TO: <#{to[l]}>\r\n"
    @responder = :receive_rcpt_to_response
  else
    e = @rcpt_responses.select {|rr| rr.last == 2}
    if e and e.length > 0
      invoke_data
    else
      invoke_error
    end
  end
end

#invoke_starttls (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 255

def invoke_starttls
  if @args[:starttls]
    # It would be more sociable to first ask if @server_caps contains
    # the string "STARTTLS" before we invoke it, but hey, life's too short.
    send_data "STARTTLS\r\n"
    @responder = :receive_starttls_response
  else
    invoke_auth
  end
end

#post_init

This method is for internal use only.
[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 172

def post_init
  @return_values = OpenStruct.new
  @return_values.start_time = Time.now
end

#receive_auth_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 300

def receive_auth_response
  return invoke_error unless @range == 2
  invoke_mail_from
end

#receive_data_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 343

def receive_data_response
  return invoke_error unless @range == 3

  # The data to send can be given in either @args[:message], @args[:content], or the
  # combination of @args[:header] and @args[:body].
  #
  #   - @args[:message] (String) MUST be a valid RFC2822 Internet Message
  #
  #   - @args[:content] (String) MUST be in correct SMTP body format, with escaped
  #     leading dots and a trailing dot line
  #
  #   - @args[:header]  (Hash or String)
  #   - @args[:body]    (Array or String)
  if @args[:message]
    send_data escape_leading_dots(@args[:message].to_s)
    send_data "\r\n.\r\n"
  elsif @args[:content]
    send_data @args[:content].to_s
  else
    # The header can be a hash or an array.
    if @args[:header].is_a?(Hash)
      (@args[:header] || {}).each {|k,v| send_data escape_leading_dots("#{k}: #{v}\r\n") }
    else
      send_data escape_leading_dots(@args[:header].to_s)
    end
    send_data "\r\n"

    if @args[:body].is_a?(Array)
      @args[:body].each {|e| send_data escape_leading_dots(e)}
    else
      send_data escape_leading_dots(@args[:body].to_s)
    end

    send_data "\r\n.\r\n"
  end

  @responder = :receive_message_response
end

#receive_ehlo_over_tls_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 275

def receive_ehlo_over_tls_response
  return invoke_error unless @range == 2
  invoke_auth
end

#receive_ehlo_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 249

def receive_ehlo_response
  return invoke_error unless @range == 2
  @server_caps = @msg
  invoke_starttls
end

#receive_line(ln)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 200

def receive_line ln
  $>.puts ln if @args[:verbose]
  @range = ln[0...1].to_i
  @code = ln[0...3].to_i
  @msg << ln[4..-1]
  unless ln[3...4] == '-'
    $>.puts @responder if @args[:verbose]
    send @responder
    @msg.clear
  end
end

#receive_mail_from_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 309

def receive_mail_from_response
  return invoke_error unless @range == 2
  invoke_rcpt_to
end

#receive_message_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 381

def receive_message_response
  return invoke_error unless @range == 2
  send_data "QUIT\r\n"
  close_connection_after_writing
  @succeeded = true
  @return_values.elapsed_time = Time.now - @return_values.start_time
  @return_values.responder = @responder
  @return_values.code = @code
  @return_values.message = @msg
  set_deferred_status :succeeded, @return_values
end

#receive_rcpt_to_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 330

def receive_rcpt_to_response
  @rcpt_responses << [@code, @msg, @range]
  invoke_rcpt_to
end

#receive_signon (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 244

def receive_signon
  return invoke_error unless @range == 2
  send_ehlo
  @responder = :receive_ehlo_response
end

#receive_starttls_response (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 265

def receive_starttls_response
  return invoke_error unless @range == 2
  start_tls
  invoke_ehlo_over_tls
end

#send_ehlo (private)

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 240

def send_ehlo
  send_data "EHLO #{@args[:domain]}\r\n"
end

#unbind

This method is for internal use only.

We can get here in a variety of ways, all of them being failures unless the @succeeded flag is set. If a protocol success was recorded, then don't set a deferred success because the caller will already have done it (no need to wait until the connection closes to invoke the callbacks).

[ GitHub ]

  
# File 'lib/em/protocols/smtpclient.rb', line 189

def unbind
  unless @succeeded
    @return_values.elapsed_time = Time.now - @return_values.start_time
    @return_values.responder = @responder
    @return_values.code = @code
    @return_values.message = @msg
    set_deferred_status(:failed, @return_values)
  end
end