post_office 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ PostOffice mock SMTP/POP3 server
2
+ ================================
3
+
4
+ By Rene van Lieshout <rene@bluerail.nl>
5
+
6
+ Usage:
7
+
8
+ sudo post_office
9
+
10
+ Listens on port 25 and 110. Configure your POP3 client with any username and password.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/post_office ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'thread'
5
+ require 'smtp_server.rb'
6
+ require 'pop_server.rb'
7
+
8
+ begin
9
+ smtp_server = Thread.new{ SMTPServer.new }
10
+ pop_server = Thread.new{ POPServer.new }
11
+
12
+ smtp_server.join
13
+ pop_server.join
14
+ rescue Interrupt
15
+ puts "Interrupt..."
16
+ rescue Errno::EACCES
17
+ puts "I need root access to open ports 25 and 110. Please sudo #{__FILE__}"
18
+ end
@@ -0,0 +1,64 @@
1
+ require 'socket'
2
+ require 'thread'
3
+
4
+ # This class starts a generic server, accepting connections
5
+ # on options[:port]
6
+ #
7
+ # When extending this class make sure you:
8
+ #
9
+ # * def greet(client)
10
+ # * def process(client, command, full_data)
11
+ # * client.close when you're done
12
+ #
13
+ # You can respond to the client using:
14
+ #
15
+ # * respond(client, text)
16
+ #
17
+ # The command given to process equals the first word
18
+ # of full_data in upcase, e.g. QUIT or LIST
19
+ #
20
+ # It's possible for multiple clients to connect at the same
21
+ # time, so use client.object_id when storing local data
22
+
23
+ class GenericServer
24
+
25
+ def initialize(options)
26
+ @port = options[:port]
27
+ server = TCPServer.open(@port)
28
+ puts "#{self.class.to_s} listening on port #{@port}"
29
+
30
+ # Accept connections until infinity and beyond
31
+ loop do
32
+ Thread.start(server.accept) do |client|
33
+ begin
34
+ greet client
35
+
36
+ # Keep processing commands until somebody closed the connection
37
+ begin
38
+ input = client.gets
39
+
40
+ # The first word of a line should contain the command
41
+ command = input.to_s.gsub(/ .*/,"").upcase.gsub(/[\r\n]/,"")
42
+
43
+ puts "#{client.object_id}:#{@port} < #{input}"
44
+
45
+ process(client, command, input)
46
+ end until client.closed?
47
+ rescue => detail
48
+ puts "#{client.object_id}:#{@port} ! #{$!}"
49
+ client.close
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Respond to client by sending back text
56
+ def respond(client, text)
57
+ puts "#{client.object_id}:#{@port} > #{text}"
58
+ client.puts text
59
+ rescue
60
+ puts "#{client.object_id}:#{@port} ! #{$!}"
61
+ client.close
62
+ end
63
+
64
+ end
data/lib/pop_server.rb ADDED
@@ -0,0 +1,182 @@
1
+ require 'generic_server.rb'
2
+ require 'store.rb'
3
+ require 'digest/md5'
4
+
5
+ # Basic POP server
6
+
7
+ class POPServer < GenericServer
8
+ # Create new server listening on port 110
9
+ def initialize
10
+ super(:port => 110)
11
+ end
12
+
13
+ # Send a greeting to client
14
+ def greet(client)
15
+ # truncate messages for this session
16
+ Store.instance.truncate
17
+
18
+ respond(client, true, "Hello there")
19
+ end
20
+
21
+ # Process command
22
+ def process(client, command, full_data)
23
+ case command
24
+ when "CAPA" then capa(client)
25
+ when "DELE" then dele(client, message_number(full_data))
26
+ when "LIST" then list(client, message_number(full_data))
27
+ when "NOOP" then respond(client, true, "Yup.")
28
+ when "PASS" then pass(client, full_data)
29
+ when "QUIT" then quit(client)
30
+ when "RETR" then retr(client, message_number(full_data))
31
+ when "RSET" then respond(client, true, "Resurrected.")
32
+ when "STAT" then stat(client)
33
+ when "TOP" then top(client, full_data)
34
+ when "UIDL" then uidl(client, message_number(full_data))
35
+ when "USER" then user(client, full_data)
36
+ else respond(client, false, "Invalid command.")
37
+ end
38
+ end
39
+
40
+ # Show the client what we can do
41
+ def capa(client)
42
+ respond(client, true, "Here's what I can do:\r\n" +
43
+ "USER\r\n" +
44
+ "IMPLEMENTATION Bluerail Post Office POP3 Server\r\n" +
45
+ ".")
46
+ end
47
+
48
+ # Accepts username
49
+ def user(client, full_data)
50
+ respond(client, true, "Password required.")
51
+ end
52
+
53
+ # Authenticates client
54
+ def pass(client, full_data)
55
+ respond(client, true, "Logged in.")
56
+ end
57
+
58
+ # Shows list of messages
59
+ #
60
+ # When a message id is specified only list
61
+ # the size of that message
62
+ def list(client, message)
63
+ if message == :invalid
64
+ respond(client, false, "Invalid message number.")
65
+ elsif message == :all
66
+ messages = ""
67
+ Store.instance.get.each.with_index do |message, index|
68
+ messages << "#{index + 1} #{message.size}\r\n"
69
+ end
70
+ respond(client, true, "POP3 clients that break here, they violate STD53.\r\n#{messages}.")
71
+ else
72
+ message_data = Store.instance.get[message - 1]
73
+ respond(client, true, "#{message} #{message_data.size}")
74
+ end
75
+ end
76
+
77
+ # Retreives message
78
+ def retr(client, message)
79
+ if message == :invalid
80
+ respond(client, false, "Invalid message number.")
81
+ elsif message == :all
82
+ respond(client, false, "Invalid message number.")
83
+ else
84
+ message_data = Store.instance.get[message - 1]
85
+ respond(client, true, "#{message_data.size} octets to follow.\r\n" + message_data + "\r\n.")
86
+ end
87
+ end
88
+
89
+ # Shows list of message uid
90
+ #
91
+ # When a message id is specified only list
92
+ # the uid of that message
93
+ def uidl(client, message)
94
+ if message == :invalid
95
+ respond(client, false, "Invalid message number.")
96
+ elsif message == :all
97
+ messages = ""
98
+ Store.instance.get.each.with_index do |message, index|
99
+ messages << "#{index + 1} #{message_uid(message)}\r\n"
100
+ end
101
+ respond(client, true, "unique-id listing follows.\r\n#{messages}.")
102
+ else
103
+ message_data = Store.instance.get[message - 1]
104
+ respond(client, true, "#{message} #{message_uid(message_data)}")
105
+ end
106
+ end
107
+
108
+ # Shows total number of messages and size
109
+ def stat(client)
110
+ messages = Store.instance.get
111
+ total_size = messages.collect{ |m| m.size }.inject(0) { |sum,x| sum+x }
112
+ respond(client, true, "#{messages.length} #{total_size}")
113
+ end
114
+
115
+ # Display headers of message
116
+ def top(client, full_data)
117
+ full_data = full_data.split(/TOP\s(\d*)/)
118
+ messagenum = full_data[1].to_i
119
+ number_of_lines = full_data[2].to_i
120
+
121
+ messages = Store.instance.get
122
+ if messages.length >= messagenum && messagenum > 0
123
+ headers = ""
124
+ line_number = -2
125
+ messages[messagenum - 1].split(/\r\n/).each do |line|
126
+ line_number = line_number + 1 if line.gsub(/\r\n/, "") == "" || line_number > -2
127
+ headers += "#{line}\r\n" if line_number < number_of_lines
128
+ end
129
+ respond(client, true, "headers follow.\r\n" + headers + "\r\n.")
130
+ else
131
+ respond(client, false, "Invalid message number.")
132
+ end
133
+ end
134
+
135
+ # Quits
136
+ def quit(client)
137
+ respond(client, true, "Better luck next time.")
138
+ client.close
139
+ end
140
+
141
+ # Deletes message
142
+ def dele(client, message)
143
+ if message == :invalid
144
+ respond(client, false, "Invalid message number.")
145
+ elsif message == :all
146
+ respond(client, false, "Invalid message number.")
147
+ else
148
+ Store.instance.remove(message - 1)
149
+ respond(client, true, "Message deleted.")
150
+ end
151
+ end
152
+
153
+ protected
154
+
155
+ # Returns message number parsed from full_data:
156
+ #
157
+ # * No message number => :all
158
+ # * Message does not exists => :invalid
159
+ # * valid message number => some fixnum
160
+ def message_number(full_data)
161
+ if /\w*\s*\d/ =~ full_data
162
+ messagenum = full_data.gsub(/\D/,"").to_i
163
+ messages = Store.instance.get
164
+ if messages.length >= messagenum && messagenum > 0
165
+ return messagenum
166
+ else
167
+ return :invalid
168
+ end
169
+ else
170
+ return :all
171
+ end
172
+ end
173
+
174
+ # Respond to client with a POP3 prefix (+OK or -ERR)
175
+ def respond(client, status, message)
176
+ super(client, "#{status ? "+OK" : "-ERR"} #{message}\r\n")
177
+ end
178
+
179
+ def message_uid(message)
180
+ Digest::MD5.hexdigest(message)
181
+ end
182
+ end
@@ -0,0 +1,134 @@
1
+ require 'generic_server.rb'
2
+ require 'store.rb'
3
+
4
+ # Basic SMTP server
5
+
6
+ class SMTPServer < GenericServer
7
+ attr_accessor :client_data
8
+
9
+ # Create new server listening on port 25
10
+ def initialize
11
+ self.client_data = Hash.new
12
+ super(:port => 25)
13
+ end
14
+
15
+ # Send a greeting to client
16
+ def greet(client)
17
+ respond(client, 220)
18
+ end
19
+
20
+ # Process command
21
+ def process(client, command, full_data)
22
+ case command
23
+ when 'DATA' then data(client)
24
+ when 'HELO', 'EHLO' then respond(client, 250)
25
+ when 'MAIL' then mail_from(client, full_data)
26
+ when 'QUIT' then quit(client)
27
+ when 'RCPT' then rcpt_to(client, full_data)
28
+ when 'RSET' then rset(client)
29
+ else begin
30
+ if get_client_data(client, :sending_data)
31
+ append_data(client, full_data)
32
+ else
33
+ respond(client, 500)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Closes connection
40
+ def quit(client)
41
+ respond(client, 221)
42
+ client.close
43
+ end
44
+
45
+ # Stores sender address
46
+ def mail_from(client, full_data)
47
+ if /^MAIL FROM:/ =~ full_data.upcase
48
+ set_client_data(client, :from, full_data.gsub(/^MAIL FROM:/i,""))
49
+ respond(client, 250)
50
+ else
51
+ respond(client, 500)
52
+ end
53
+ end
54
+
55
+ # Stores recepient address
56
+ def rcpt_to(client, full_data)
57
+ if /^RCPT TO:/ =~ full_data.upcase
58
+ set_client_data(client, :to, full_data.gsub(/^RCPT TO:/i,""))
59
+ respond(client, 250)
60
+ else
61
+ respond(client, 500)
62
+ end
63
+ end
64
+
65
+ # Markes client sending data
66
+ def data(client)
67
+ set_client_data(client, :sending_data, true)
68
+ respond(client, 354)
69
+ end
70
+
71
+ # Resets local client store
72
+ def rset(client)
73
+ self.client_data[client.object_id] = Hash.new
74
+ end
75
+
76
+ # Adds full_data to incoming mail message
77
+ #
78
+ # We'll store the mail when full_data == "."
79
+ def append_data(client, full_data)
80
+ if full_data.gsub(/[\r\n]/,"") == "."
81
+ Store.instance.add(
82
+ get_client_data(client, :from).to_s,
83
+ get_client_data(client, :to).to_s,
84
+ get_client_data(client, :data).to_s
85
+ )
86
+ respond(client, 250)
87
+ else
88
+ set_client_data(client, :data, get_client_data(client, :data).to_s + full_data)
89
+ end
90
+ end
91
+
92
+ protected
93
+
94
+ # Store key value combination for this client
95
+ def set_client_data(client, key, value)
96
+ self.client_data[client.object_id] = Hash.new unless self.client_data.include?(client.object_id)
97
+ self.client_data[client.object_id][key] = value
98
+ end
99
+
100
+ # Retreive key from local client store
101
+ def get_client_data(client, key)
102
+ self.client_data[client.object_id][key] if self.client_data.include?(client.object_id)
103
+ end
104
+
105
+ # Respond to client using a standard SMTP response code
106
+ def respond(client, code)
107
+ super(client, "#{code} #{SMTPServer::RESPONSES[code].to_s}")
108
+ end
109
+
110
+ # Standard SMTP response codes
111
+ RESPONSES = {
112
+ 500 => "Syntax error, command unrecognized",
113
+ 501 => "Syntax error in parameters or arguments",
114
+ 502 => "Command not implemented",
115
+ 503 => "Bad sequence of commands",
116
+ 504 => "Command parameter not implemented",
117
+ 211 => "System status, or system help respond",
118
+ 214 => "Help message",
119
+ 220 => "Bluerail Post Office Service ready",
120
+ 221 => "Bluerail Post Office Service closing transmission channel",
121
+ 421 => "Bluerail Post Office Service not available,",
122
+ 250 => "Requested mail action okay, completed",
123
+ 251 => "User not local; will forward to <forward-path>",
124
+ 450 => "Requested mail action not taken: mailbox unavailable",
125
+ 550 => "Requested action not taken: mailbox unavailable",
126
+ 451 => "Requested action aborted: error in processing",
127
+ 551 => "User not local; please try <forward-path>",
128
+ 452 => "Requested action not taken: insufficient system storage",
129
+ 552 => "Requested mail action aborted: exceeded storage allocation",
130
+ 553 => "Requested action not taken: mailbox name not allowed",
131
+ 354 => "Start mail input; end with <CRLF>.<CRLF>",
132
+ 554 => "Transaction failed"
133
+ }.freeze
134
+ end
data/lib/store.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'fileutils'
2
+ require 'singleton'
3
+
4
+ # Message storage
5
+
6
+ class Store
7
+ include Singleton
8
+ attr_accessor :messages
9
+
10
+ def initialize
11
+ self.messages = []
12
+ end
13
+
14
+ # Returns array of messages
15
+ def get
16
+ return messages
17
+ end
18
+
19
+ # Saves message in storage
20
+ def add(mail_from, rcpt_to, message_data)
21
+ messages.push message_data
22
+ end
23
+
24
+ # Removes message from storage
25
+ def remove(index)
26
+ self.messages[index] = nil
27
+ end
28
+
29
+ # Remove empty messages
30
+ def truncate
31
+ self.messages = self.messages.reject{ |message| message.nil? }
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: post_office
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Rene van Lieshout
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-18 00:00:00 +02:00
19
+ default_executable: post_office
20
+ dependencies: []
21
+
22
+ description: A mock SMTP/POP3 server to aid in the development of applications with mail functionality.
23
+ email: rene@bluerail.nl
24
+ executables:
25
+ - post_office
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.md
30
+ files:
31
+ - README.md
32
+ - VERSION
33
+ - lib/generic_server.rb
34
+ - lib/pop_server.rb
35
+ - lib/smtp_server.rb
36
+ - lib/store.rb
37
+ - bin/post_office
38
+ has_rdoc: true
39
+ homepage: http://github.com/bluerail/post_office
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ hash: 3
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.7
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: PostOffice mock SMTP/POP3 server
72
+ test_files: []
73
+