post_office 0.1.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.
- data/README.md +10 -0
- data/VERSION +1 -0
- data/bin/post_office +18 -0
- data/lib/generic_server.rb +64 -0
- data/lib/pop_server.rb +182 -0
- data/lib/smtp_server.rb +134 -0
- data/lib/store.rb +33 -0
- metadata +73 -0
data/README.md
ADDED
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
|
data/lib/smtp_server.rb
ADDED
@@ -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
|
+
|