celluloid-smtp 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c5a69f2ed269acb76e89ee43e1e60412dc5a3a03
4
+ data.tar.gz: ce63f363549dc3772f9e4196801a7b4ea41d14dc
5
+ SHA512:
6
+ metadata.gz: 3ee9844f6d97980fdda68a8cd9d5f7f41c5ffc410a95ffdb44a086fb3ea206ca51e9e0adbc67f8ad9a68b5504c81c6a1f5fb18369400b912f67802517cc35b81
7
+ data.tar.gz: 7d222884724f3df4ebfaef75ab5c1409307606b909b23f6e21aa7c9c37fb8cd8f30b1eca8ddea2dc4587b690f3dcdce35ef4848451304061057a8133a1b5bfa4
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ .DS_Store
3
+ *.log
4
+ pkg/*
@@ -0,0 +1,3 @@
1
+ [submodule "culture"]
2
+ path = culture
3
+ url = http://github.com/celluloid/culture.git
@@ -0,0 +1,2 @@
1
+ 0.0.0.9
2
+ - Decided to remove "block form" compatibility for now, in favor of overriding various events.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'celluloid', github: 'celluloid/celluloid', branch: '0.17.0-prerelease', submodules: true
6
+ gem 'celluloid-io', github: 'celluloid/celluloid-io', branch: '0.17.0-dependent', submodules: true
7
+
8
+ #de For debugging. Watching for thread leaks.
9
+ gem 'cellumon', github: 'digitalextremist/cellumon', branch: 'master'
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 //de ( digitalextremist // )
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # cellulloid-smtp
2
+
3
+ Evented, actor-based SMTP server built on [Celluloid::IO](http://github.com/celluloid/celluloid-io).
4
+
5
+ Inspired by [midi-smtp-server](https://github.com/4commerce-technologies-AG/midi-smtp-server),
6
+ but mostly based on [Reel](http://github.com/celluloid/reel).
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ require File.expand_path("../culture/sync", __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'celluloid-smtp'
5
+ gem.version = Celluloid::SMTP::VERSION
6
+
7
+ gem.summary = "Celluloid based SMTP server."
8
+ gem.description = "A small, fast, evented, actor-based, highly customizable Ruby SMTP server."
9
+
10
+ gem.authors = ["digitalextremist //"]
11
+ gem.email = 'code@extremist.digital'
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.require_paths = ["lib"]
16
+
17
+ gem.homepage = 'https://github.com/abstractive/celluloid-smtp'
18
+ gem.license = 'MIT'
19
+
20
+ gem.add_development_dependency "mail"
21
+ end
File without changes
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
2
+
3
+ require 'bundler/setup'
4
+ require 'celluloid/smtp'
5
+
6
+ at_exit {
7
+ Celluloid::Internals::Logger.info "Shutting down SMTP server."
8
+ Celluloid[:smtpd].async.shutdown
9
+ sleep 0.126 * 2
10
+ Celluloid.shutdown
11
+ }
12
+
13
+ begin
14
+ Celluloid::SMTP::Server.run
15
+ rescue Interrupt
16
+ exit
17
+ end
@@ -0,0 +1,78 @@
1
+ $LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
2
+
3
+ require 'bundler/setup'
4
+ require 'celluloid/current'
5
+ require 'mail'
6
+
7
+ TESTS = 1000
8
+ THREADS = 4
9
+
10
+ HOST = ARGV[0] || "localhost"
11
+ PORT = (ARGV[1] || 2525).to_i
12
+ TO = ARGV[2] || "smtp@celluloid.io"
13
+ FROM = ARGV[3] || TO
14
+
15
+ Mail.defaults do
16
+ delivery_method :smtp, address: HOST, port: PORT
17
+ end
18
+
19
+ fail "No TO address specified." unless TO
20
+
21
+ puts "Simulating sending #{TESTS}x#{THREADS} messages through #{HOST}@#{PORT}."
22
+
23
+ class Sender
24
+ include Celluloid
25
+ def tests(count)
26
+ times = count.times.map { future.test }
27
+ values = times.map(&:value)
28
+ failures = values.select { |v| v == :fail }.count
29
+ values.select! { |v| v != :fail }
30
+ average = values.inject{ |sum, t| sum + t }.to_f / values.size
31
+ return [0, failures] if failures == count
32
+ [average,failures]
33
+ end
34
+ def test
35
+ start = Time.now
36
+ mail = Mail.new do
37
+ from FROM
38
+ to TO
39
+ subject 'Test email.'
40
+ body "Test message.... #{Time.now}"
41
+ end
42
+
43
+ begin
44
+ mail.deliver
45
+ print "."
46
+ return Time.now.to_f - start.to_f
47
+ rescue => ex
48
+ print "!"
49
+ :fail
50
+ end
51
+ end
52
+ end
53
+
54
+ senders = THREADS.times.map { Sender.new }
55
+
56
+ start = Time.now
57
+ tests = senders.map {|sender| sender.future.tests(TESTS) }
58
+
59
+ values = tests.map(&:value)
60
+ total = Time.now-start
61
+ total_fail = false
62
+
63
+ puts "\n\n\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
64
+ failures = values.map { |v| v[1] }.inject{ |sum,f| sum + f }
65
+ average = values.map { |v| v[0] }.inject{ |sum, t| sum + t }.to_f / values.size
66
+ overall_average = total/(TESTS*THREADS)
67
+ if total_fail
68
+ puts "\n All #{THREADS*TESTS} messages failed."
69
+ total_fail = true
70
+ end
71
+ puts "\n Failures: #{failures}"
72
+ puts "\n Average time for #{TESTS}x#{THREADS} messages:"
73
+ puts " ~#{"%0.4f" % average} seconds per message, actual estimate." unless total_fail
74
+ puts " ~#{"%0.4f" % (overall_average)} of overall runtime, per message."
75
+ puts "\n Total time running test: #{"%0.4f" % total} seconds:"
76
+ puts " ~#{"%0.4f" % (1/average)} messages per second, actual estimate." unless total_fail
77
+ puts " ~#{"%0.4f" % (1/overall_average)} messages per second, overall."
78
+ puts "\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\n\n"
@@ -0,0 +1,56 @@
1
+ $LOAD_PATH.push(File.expand_path("../../lib", __FILE__))
2
+
3
+ require 'bundler/setup'
4
+ require 'mail'
5
+
6
+ INTERVAL = 2.22
7
+
8
+ Mail.defaults do
9
+ delivery_method :smtp, address: "localhost", port: 2525
10
+ end
11
+
12
+ TO = "smtp@celluloid.io"
13
+ FROM = TO
14
+
15
+ fail "No TO address specified." unless TO
16
+
17
+ puts "Simulating sending a message every #{INTERVAL} seconds."
18
+
19
+ futures = []
20
+ @mutex = Mutex.new
21
+ begin
22
+ Thread.new {
23
+ @mutex.synchronize {
24
+ loop {
25
+ futures = Thread.new {
26
+ start = Time.now
27
+ mail = Mail.new do
28
+ from FROM
29
+ to TO
30
+ subject 'Test email.'
31
+ body "Test message.... #{start}"
32
+ end
33
+
34
+ begin
35
+ mail.deliver
36
+ print "|"
37
+ rescue Errno::ECONNREFUSED
38
+ print "X"
39
+ rescue EOFError
40
+ print "?"
41
+ rescue => ex
42
+ print "!"
43
+ STDERR.puts "Error communicating with server: #{ex} (#{ex.class})"
44
+ end
45
+ }
46
+ sleep INTERVAL
47
+ }
48
+ }
49
+ }
50
+
51
+ loop {
52
+ future = @mutex.synchronize { futures.shift }.value rescue nil
53
+ }
54
+ rescue Interrupt
55
+ puts "Done testing."
56
+ end
@@ -0,0 +1,26 @@
1
+ require 'celluloid/current'
2
+ require 'celluloid/io'
3
+
4
+ module Celluloid
5
+ module SMTP
6
+ require 'celluloid/smtp/constants'
7
+ require 'celluloid/smtp/logging'
8
+ require 'celluloid/smtp/version'
9
+ require 'celluloid/smtp/extensions'
10
+ class Server
11
+ include SMTP::Extensions
12
+ require 'celluloid/smtp/server'
13
+ require 'celluloid/smtp/server/protector'
14
+ require 'celluloid/smtp/server/handler'
15
+ require 'celluloid/smtp/server/transporter'
16
+ end
17
+ class Connection
18
+ include SMTP::Extensions
19
+ require 'celluloid/smtp/connection/errors'
20
+ require 'celluloid/smtp/connection/events'
21
+ require 'celluloid/smtp/connection/parser'
22
+ require 'celluloid/smtp/connection/automata'
23
+ require 'celluloid/smtp/connection'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ class Celluloid::SMTP::Connection
2
+
3
+ attr_reader :socket, :automata
4
+
5
+ extend Forwardable
6
+ def_delegators :@socket, :close, :peeraddr, :print, :closed?
7
+ def_delegators :@automata, :transition
8
+
9
+ def initialize(socket, configuration)
10
+ @automata = Automata.new(self)
11
+ @configuration = configuration.dup
12
+ @socket = socket
13
+ @timestamps = {}
14
+ @context = nil
15
+ @behavior = configuration.fetch(:behavior, DEFAULT_BEHAVIOR)
16
+ transition :connection
17
+ end
18
+
19
+ def start!
20
+ @timestamps[:start] = Time.now
21
+ end
22
+
23
+ def finish!
24
+ @timestamps[:finish] = Time.now
25
+ end
26
+
27
+ def length
28
+ raise "Connection incomplete." unless @timestamps[:start] && @timestamps[:finish]
29
+ @timestamps[:finish].to_f - @timestamps[:start].to_f
30
+ end
31
+
32
+ def relaying?
33
+ @behavior == :relay
34
+ end
35
+
36
+ def delivering?
37
+ @behavior == :deliver
38
+ end
39
+
40
+ def print!(string)
41
+ print "#{string}\r\n"
42
+ end
43
+
44
+ def remote_ip
45
+ peeraddr(false)[3]
46
+ end
47
+ alias remote_addr remote_ip
48
+
49
+ def remote_host
50
+ # NOTE: This is currently a blocking operation.
51
+ peeraddr(true)[2]
52
+ end
53
+
54
+ end
@@ -0,0 +1,70 @@
1
+ class Celluloid::SMTP::Connection::Automata
2
+ include Celluloid::FSM
3
+ include Celluloid::SMTP::Extensions
4
+ def_delegators :@connection,
5
+ :delivering?,
6
+ :relaying?,
7
+ :closed?,
8
+ :start!,
9
+ :finish!,
10
+ :print,
11
+ :length,
12
+ :event!,
13
+ :handle!
14
+
15
+ def initialize(connection)
16
+ @connection = connection
17
+ end
18
+
19
+ default_state :initialize
20
+
21
+ state :connection, :to => [:handling, :closed] do
22
+ start!
23
+ event!(:on_connection)
24
+ transition :handling
25
+ end
26
+
27
+ state :handling, :to => [:handled, :disconnecting, :closed] do
28
+ debug "Parsing message." if DEBUG_AUTOMATA
29
+ handle!
30
+ end
31
+
32
+ state :handled, :to => [:relaying, :delivering, :disconnecting, :closed] do
33
+ debug "Finished handling." if DEBUG_AUTOMATA
34
+ if relaying?
35
+ transition :relaying
36
+ elsif delivering?
37
+ transition :delivering
38
+ else
39
+ transition :disconnecting
40
+ end
41
+ end
42
+
43
+ state :relaying, to: [:relayed, :closed] do
44
+ debug "Relaying message." if DEBUG_AUTOMATA
45
+ end
46
+
47
+ state :relayed, to: [:disconnecting, :closed] do
48
+ debug "Message relayed." if DEBUG_AUTOMATA
49
+ end
50
+
51
+ state :delivering, to: [:delivered, :closed] do
52
+ debug "Delivering message." if DEBUG_AUTOMATA
53
+
54
+ end
55
+
56
+ state :delivered, to: [:disconnecting, :closed] do
57
+ debug "Message delivered." if DEBUG_AUTOMATA
58
+ end
59
+
60
+ state :disconnecting, to: [:closed] do
61
+ event!(:on_disconnect)
62
+ transition :closed
63
+ end
64
+
65
+ state :closed, :to => [] do
66
+ close unless closed? rescue nil
67
+ finish!
68
+ debug "TIMER: #{"%0.4f" % length} on connection." if DEBUG_TIMING
69
+ end
70
+ end
@@ -0,0 +1,97 @@
1
+ module Celluloid::SMTP
2
+ class Exception < ::Exception
3
+ attr_reader :code, :text
4
+ def initialize(msg=nil, code, text)
5
+ @code = code
6
+ @text = text
7
+ super msg
8
+ end
9
+ def result
10
+ "#{@code} #{@text}"
11
+ end
12
+ end
13
+
14
+ class Error421 < Exception
15
+ def initialize(msg="")
16
+ super msg, 421, "Service not available, closing transmission channel"
17
+ end
18
+ end
19
+
20
+ class Error450 < Exception
21
+ def initialize(msg="")
22
+ super msg, 450, "Requested mail action not taken: mailbox unavailable"
23
+ end
24
+ end
25
+
26
+ class Error451 < Exception
27
+ def initialize(msg="")
28
+ super msg, 451, "Requested action aborted: local error in processing"
29
+ end
30
+ end
31
+
32
+ class Error452 < Exception
33
+ def initialize(msg="")
34
+ super msg, 452, "Requested action not taken: insufficient system storage"
35
+ end
36
+ end
37
+
38
+ class Error500 < Exception
39
+ def initialize(msg="")
40
+ super msg, 500, "Syntax error, command unrecognised or error in parameters or arguments"
41
+ end
42
+ end
43
+
44
+ class Error501 < Exception
45
+ def initialize(msg="")
46
+ super msg, 501, "Syntax error in parameters or arguments"
47
+ end
48
+ end
49
+
50
+ class Error502 < Exception
51
+ def initialize(msg="")
52
+ super msg, 502, "Command not implemented"
53
+ end
54
+ end
55
+
56
+ class Error503 < Exception
57
+ def initialize(msg="")
58
+ super msg, 503, "Bad sequence of commands"
59
+ end
60
+ end
61
+
62
+ class Error504 < Exception
63
+ def initialize(msg="")
64
+ super msg, 504, "Command parameter not implemented"
65
+ end
66
+ end
67
+
68
+ class Error521 < Exception
69
+ def initialize(msg="")
70
+ super msg, 521, "Service does not accept mail"
71
+ end
72
+ end
73
+
74
+ class Error550 < Exception
75
+ def initialize(msg="")
76
+ super msg, 550, "Requested action not taken: mailbox unavailable"
77
+ end
78
+ end
79
+
80
+ class Error552 < Exception
81
+ def initialize(msg="")
82
+ super msg, 552, "Requested mail action aborted: exceeded storage allocation"
83
+ end
84
+ end
85
+
86
+ class Error553 < Exception
87
+ def initialize(msg="")
88
+ super msg, 553, "Requested action not taken: mailbox name not allowed"
89
+ end
90
+ end
91
+
92
+ class Error554 < Exception
93
+ def initialize(msg="")
94
+ super msg, 554, "Transaction failed"
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ class Celluloid::SMTP::Connection
2
+
3
+ def event!(method,*args)
4
+ start = Time.now
5
+ debug("Executing event: #{method}") if DEBUG_EVENTS
6
+ result = send(method,*args)
7
+ debug("TIMER: #{"%0.4f" %(Time.now-start)} on event: #{method}") if DEBUG_TIMING
8
+ result
9
+ rescue => ex
10
+ exception(ex, "Failure in event processor: #{method}")
11
+ nil
12
+ end
13
+
14
+ def on_connection
15
+ debug("Client connected.") if DEBUG_EVENTS
16
+ end
17
+
18
+ def on_disconnect
19
+ debug("Disconnecting client.") if DEBUG_EVENTS
20
+ end
21
+
22
+ def on_helo(helo)
23
+ debug("HELO: #{helo}") if DEBUG_EVENTS
24
+ return helo
25
+ end
26
+
27
+ def on_mail_from(from)
28
+ debug("MAIL FROM: #{from}") if DEBUG_EVENTS
29
+ return from
30
+ end
31
+
32
+ def on_rcpt_to(to)
33
+ debug("RCPT TO: #{to}") if DEBUG_EVENTS
34
+ return to
35
+ end
36
+
37
+ def on_message(message)
38
+ debug("MESSAGE") if DEBUG_EVENTS
39
+ return message[:data]
40
+ end
41
+ end
@@ -0,0 +1,184 @@
1
+ class Celluloid::SMTP::Connection
2
+
3
+ include Celluloid::SMTP
4
+
5
+ def handle!
6
+ context!(true)
7
+ if process!
8
+ transition :handled
9
+ else
10
+ transition :disconnecting
11
+ end
12
+ rescue => ex
13
+ exception(ex, "Error handling command session")
14
+ transition :closed
15
+ end
16
+
17
+ [:data, :quit, :helo, :mail, :rcpt, :rset].each { |command|
18
+ define_method(:"#{command}?") { @sequence == command }
19
+ define_method(:"#{command}!") { @sequence = command }
20
+ }
21
+
22
+ def context!(first=false)
23
+ @helo = nil if first
24
+ @sequence = first ? :helo : :rset
25
+ @context = {} if first || @context.nil?
26
+ @context[:envelope] = {from: "", to: []}
27
+ @context[:message] = {delivered: -1, bytesize: -1, data: ""}
28
+ end
29
+
30
+ def envelope; @context[:envelope] ||= {} end
31
+ def message; @context[:message] ||= {} end
32
+
33
+ def process!
34
+ print! "220 #{@configuration[:hostname]} says welcome!"
35
+ begin
36
+ loop {
37
+ output = begin
38
+ data = @socket.readline
39
+ debug(">> #{data.chomp}") unless data? if DEBUG
40
+ line! data
41
+ rescue Celluloid::SMTP::Exception => ex
42
+ exception(ex, "Processing error")
43
+ rescue => ex
44
+ exception(ex, "Unknown exception")
45
+ Error500.new.result
46
+ end
47
+ unless output.empty?
48
+ debug("<< #{output}") if DEBUG
49
+ print! output
50
+ end
51
+ break if quit? || closed?
52
+ }
53
+ print! "221 Service closing transmission channel" unless closed?
54
+ return true
55
+ rescue EOFError
56
+ debug("Lost connection due to client abort.")
57
+ rescue Exception => ex
58
+ exception(ex, "Error parsing command session")
59
+ print! Error421.new.result unless closed?
60
+ end
61
+ false
62
+ end
63
+
64
+ def line!(data)
65
+ unless data?
66
+ case data
67
+ when (/^(HELO|EHLO)(\s+.*)?$/i) # HELO/EHLO
68
+ # 250 Requested mail action okay, completed
69
+ # 421 <domain> Service not available, closing transmission channel
70
+ # 500 Syntax error, command unrecognised
71
+ # 501 Syntax error in parameters or arguments
72
+ # 504 Command parameter not implemented
73
+ # 521 <domain> does not accept mail [rfc1846]
74
+ raise Error503 unless helo?
75
+ data = data.gsub(/^(HELO|EHLO)\ /i, '').strip
76
+ if return_value = event!(:on_helo, data)
77
+ data = return_value
78
+ end
79
+ @helo = data
80
+ rset!
81
+ return "250 OK"
82
+ when (/^NOOP\s*$/i) # NOOP
83
+ # 250 Requested mail action okay, completed
84
+ # 421 <domain> Service not available, closing transmission channel
85
+ # 500 Syntax error, command unrecognised
86
+ return "250 OK"
87
+ when (/^RSET\s*$/i) # RSET
88
+ # 250 Requested mail action okay, completed
89
+ # 421 <domain> Service not available, closing transmission channel
90
+ # 500 Syntax error, command unrecognised
91
+ # 501 Syntax error in parameters or arguments
92
+ raise Error503 if helo?
93
+ # handle command
94
+ context!
95
+ return "250 OK"
96
+ when (/^QUIT\s*$/i) # QUIT
97
+ # 221 <domain> Service closing transmission channel
98
+ # 500 Syntax error, command unrecognised
99
+ quit!
100
+ return ""
101
+ when (/^MAIL FROM\:/i) # MAIL
102
+ # 250 Requested mail action okay, completed
103
+ # 421 <domain> Service not available, closing transmission channel
104
+ # 451 Requested action aborted: local error in processing
105
+ # 452 Requested action not taken: insufficient system storage
106
+ # 500 Syntax error, command unrecognised
107
+ # 501 Syntax error in parameters or arguments
108
+ # 552 Requested mail action aborted: exceeded storage allocation
109
+ raise Error503 unless rset?
110
+ data = data.gsub(/^MAIL FROM\:/i, '').strip
111
+ if return_value = event!(:on_mail_from, data)
112
+ data = return_value
113
+ end
114
+ envelope[:from] = data
115
+ mail!
116
+ return "250 OK"
117
+ when (/^RCPT TO\:/i) # RCPT
118
+ # 250 Requested mail action okay, completed
119
+ # 251 User not local; will forward to <forward-path>
120
+ # 421 <domain> Service not available, closing transmission channel
121
+ # 450 Requested mail action not taken: mailbox unavailable
122
+ # 451 Requested action aborted: local error in processing
123
+ # 452 Requested action not taken: insufficient system storage
124
+ # 500 Syntax error, command unrecognised
125
+ # 501 Syntax error in parameters or arguments
126
+ # 503 Bad sequence of commands
127
+ # 521 <domain> does not accept mail [rfc1846]
128
+ # 550 Requested action not taken: mailbox unavailable
129
+ # 551 User not local; please try <forward-path>
130
+ # 552 Requested mail action aborted: exceeded storage allocation
131
+ # 553 Requested action not taken: mailbox name not allowed
132
+ raise Error503 unless mail? || rset?
133
+ data = data.gsub(/^RCPT TO\:/i, '').strip
134
+ if return_value = event!(:on_rcpt_to, data)
135
+ data = return_value
136
+ end
137
+ envelope[:to] << data
138
+ rset!
139
+ return "250 OK"
140
+ when (/^DATA\s*$/i) # DATA
141
+ # 354 Start mail input; end with <CRLF>.<CRLF>
142
+ # 250 Requested mail action okay, completed
143
+ # 421 <domain> Service not available, closing transmission channel received data
144
+ # 451 Requested action aborted: local error in processing
145
+ # 452 Requested action not taken: insufficient system storage
146
+ # 500 Syntax error, command unrecognised
147
+ # 501 Syntax error in parameters or arguments
148
+ # 503 Bad sequence of commands
149
+ # 552 Requested mail action aborted: exceeded storage allocation
150
+ # 554 Transaction failed
151
+ raise Error503 unless rset?
152
+ data!
153
+ return "354 Enter message, ending with \".\" on a data by itself"
154
+ else
155
+ raise Error500
156
+ end
157
+ else # Data mode.
158
+ if (data.chomp =~ /^\.$/) # Line with only a period; being told to exit data mode.
159
+ message[:data] += data
160
+ message[:data].gsub!(/\r\n\Z/, '').gsub!(/\.\Z/, '') # remove ending line .
161
+ begin
162
+ if return_value = event!(:on_message, @context)
163
+ message[:data] = return_value
164
+ end
165
+ message[:delivered] = Time.now.utc # save delivered time
166
+ message[:bytesize] = message[:data].bytesize # save bytesize of message data
167
+ return "250 Requested mail action okay, completed"
168
+
169
+ rescue Celluloid::SMTP::Exception
170
+ raise
171
+ rescue Exception => ex
172
+ raise Error451.new("#{ex}")
173
+ ensure
174
+ context!
175
+ end
176
+ else
177
+ message[:data] += data
178
+ return ""
179
+ end
180
+
181
+ end
182
+ end
183
+
184
+ end
@@ -0,0 +1,15 @@
1
+ module Celluloid::SMTP::Constants
2
+ DEBUG = false
3
+ DEBUG_TIMING = true
4
+ DEBUG_AUTOMATA = false
5
+ DEBUG_EVENTS = false
6
+
7
+ HANDLERS = 0
8
+ LOGGER = Celluloid::Internals::Logger
9
+ DEFAULT_HOST = '127.0.0.1'
10
+ DEFAULT_PORT = 2525
11
+ DEFAULT_BACKLOG = 100
12
+ DEFAULT_BEHAVIOR = :blackhole
13
+ DEFAULT_HOSTNAME = "localhost"
14
+ TIMEOUT = 9
15
+ end
@@ -0,0 +1,7 @@
1
+ module Celluloid::SMTP::Extensions
2
+ def self.included(object)
3
+ object.send(:include, Celluloid::SMTP::Constants)
4
+ object.extend Forwardable
5
+ object.def_delegators :"Celluloid::SMTP::Server.logger", :debug, :console, :warn, :error, :exception
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ class Celluloid::SMTP::Logging
2
+ include Celluloid
3
+
4
+ [:debug, :info, :warn, :error].each { |method|
5
+ define_method(method) { |*args| async.log(method,*args) }
6
+ }
7
+
8
+ alias :console :info
9
+
10
+ def log(method, *args)
11
+ Celluloid::SMTP::Constants::LOGGER.send(method,*args)
12
+ end
13
+
14
+ def exception(ex, note)
15
+ error("#{note}: #{ex} (#{ex.class})")
16
+ ex.backtrace.each { |line| error("* #{line}") }
17
+ end
18
+ end
@@ -0,0 +1,94 @@
1
+ module Celluloid
2
+ module SMTP
3
+ class Server
4
+ include Celluloid::IO
5
+ class << self
6
+ @logger = nil
7
+ attr_accessor :logger
8
+ def launch(options)
9
+ unless @logger
10
+ Celluloid::SMTP::Logging.supervise as: :logger
11
+ @logger = Celluloid[:logger]
12
+ end
13
+ if SMTP::Constants::HANDLERS > 0
14
+ Celluloid::SMTP::Server::Handler.supervise as: :handler, size: SMTP::Constants::HANDLERS
15
+ end
16
+ supervise(as: :smtpd, args:[options])
17
+ Celluloid[:smtpd]
18
+ end
19
+ def run!(options={})
20
+ launch(options)
21
+ end
22
+ def run(options={})
23
+ launch(options)
24
+ sleep
25
+ end
26
+ end
27
+
28
+ finalizer :ceased
29
+
30
+ def ceased
31
+ @server.close rescue nil
32
+ warn "SMTP Server offline."
33
+ end
34
+
35
+ def initialize(options={})
36
+ @options = options
37
+ @host = options.fetch(:host, DEFAULT_HOST)
38
+ @port = options.fetch(:port, DEFAULT_PORT)
39
+ @behavior = options.fetch(:behavior, DEFAULT_BEHAVIOR)
40
+ @hostname = options.fetch(:hostname, DEFAULT_HOSTNAME)
41
+ @backlog = options.fetch(:backlog, DEFAULT_BACKLOG)
42
+
43
+ @server = Celluloid::IO::TCPServer.new(@host, @port)
44
+ @server.listen(options.fetch(:backlog, @backlog))
45
+
46
+ console("Celluloid::IO SMTP Server #{SMTP::VERSION} @ #{@host}:#{@port}")
47
+
48
+ @options[:rescue] ||= []
49
+ @options[:rescue] += [
50
+ Errno::ECONNRESET,
51
+ Errno::EPIPE,
52
+ Errno::EINPROGRESS,
53
+ Errno::ETIMEDOUT,
54
+ Errno::EHOSTUNREACH
55
+ ]
56
+ async.run
57
+ end
58
+
59
+ def shutdown
60
+ @online = false
61
+ sleep 0.126
62
+ @server.close if @server rescue nil
63
+ end
64
+
65
+ private
66
+
67
+ def run
68
+ console "Starting to handle SMTP connections, with #{HANDLERS} handlers."
69
+ @online = true
70
+ loop {
71
+ break unless @online
72
+ begin
73
+ socket = @server.accept
74
+ rescue *@options[:rescue] => ex
75
+ warn "Error accepting socket: #{ex.class}: #{ex.to_s}"
76
+ next
77
+ rescue IOError, EOFError
78
+ warn "I/O Error on socket: #{ex.class}: #{ex.to_s}"
79
+ end
80
+ async.connection(socket)
81
+ }
82
+ end
83
+
84
+ def connection(socket)
85
+ if HANDLERS > 0
86
+ Celluloid[:handler].socket(socket, @options)
87
+ else
88
+ self.class.protector(socket) { |io| Connection.new(io, @options) }
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,11 @@
1
+ class Celluloid::SMTP::Server::Handler
2
+ include Celluloid
3
+ def socket(socket, options)
4
+ async.serve(socket,options)
5
+ end
6
+ def serve(socket, options)
7
+ SMTP::Server.protector(socket) { |io|
8
+ SMTP::Connection.new(io,options)
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ class Celluloid::SMTP::Server
2
+ class << self
3
+ include Celluloid::SMTP::Extensions
4
+ def protector(io)
5
+ Thread.new {
6
+ Timeout.timeout(TIMEOUT) {
7
+ yield(io)
8
+ }
9
+ }.value
10
+ rescue EOFError, IOError
11
+ warn "Premature disconnect."
12
+ rescue Timeout::Error
13
+ warn "Timeout handling connection."
14
+ rescue Exception => ex
15
+ exception(ex, "Unknown connection error")
16
+ ensure
17
+ io.close
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ class Celluloid::SMTP::Server::Transporter
2
+ include Celluloid::IO
3
+
4
+ def deliver(message)
5
+ end
6
+
7
+ def relay(message)
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Celluloid::SMTP
2
+ VERSION = "0.9.0"
3
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: celluloid-smtp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - digitalextremist //
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '>='
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ name: mail
20
+ prerelease: false
21
+ type: :development
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: A small, fast, evented, actor-based, highly customizable Ruby SMTP server.
28
+ email: code@extremist.digital
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - .gitmodules
35
+ - CHANGES.md
36
+ - Gemfile
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - celluloid-smtp.gemspec
41
+ - events.rb
42
+ - examples/basic_server.rb
43
+ - examples/test_load.rb
44
+ - examples/test_messages.rb
45
+ - lib/celluloid/smtp.rb
46
+ - lib/celluloid/smtp/connection.rb
47
+ - lib/celluloid/smtp/connection/automata.rb
48
+ - lib/celluloid/smtp/connection/errors.rb
49
+ - lib/celluloid/smtp/connection/events.rb
50
+ - lib/celluloid/smtp/connection/parser.rb
51
+ - lib/celluloid/smtp/constants.rb
52
+ - lib/celluloid/smtp/extensions.rb
53
+ - lib/celluloid/smtp/logging.rb
54
+ - lib/celluloid/smtp/server.rb
55
+ - lib/celluloid/smtp/server/handler.rb
56
+ - lib/celluloid/smtp/server/protector.rb
57
+ - lib/celluloid/smtp/server/transporter.rb
58
+ - lib/celluloid/smtp/version.rb
59
+ homepage: https://github.com/abstractive/celluloid-smtp
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.4.6
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Celluloid based SMTP server.
83
+ test_files: []