-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathcommand_router.rb
More file actions
182 lines (157 loc) · 5.91 KB
/
command_router.rb
File metadata and controls
182 lines (157 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# frozen_string_literal: true
require "base64"
class Net::IMAP::FakeServer
# :nodoc:
class CommandRouter
module Routable
def on(*command_names, &handler)
scope = self.is_a?(Module) ? self : singleton_class
command_names.each do |command_name|
method_name = :"handle_#{command_name.downcase}"
scope.undef_method(method_name) if scope.method_defined?(method_name)
scope.define_method(method_name, &handler)
end
end
end
include Routable
extend Routable
def initialize(writer, config:, state:)
@config = config
@state = state
@writer = writer
end
def commands; state.commands end
def handle(command)
commands << command
resp = @writer.for_command(command)
handler = handler_for(command) or return resp.fail_no_command
handler.call(resp)
end
alias << handle
def handler_for(command)
hname = command.name.downcase.to_sym
mname = :"handle_#{hname}"
config.handlers[hname] || (method(mname) if respond_to?(mname))
end
on "CAPABILITY" do |resp|
resp.args.nil? or return resp.fail_bad_args
resp.untagged :CAPABILITY, state.capabilities(config)
resp.done_ok
end
on "NOOP" do |resp|
resp.args.nil? or return resp.fail_bad_args
resp.done_ok
end
on "LOGOUT" do |resp|
resp.args.nil? or return resp.fail_bad_args
resp.bye
state.logout
begin
resp.done_ok
rescue IOError
# TODO: fix whatever is causing this!
warn "connection issue after bye but before LOGOUT could complete"
if $!.respond_to? :detailed_message
warn $!.detailed_message highlight: true, order: :bottom
else
warn $!.full_message highlight: true, order: :bottom
end
end
end
on "STARTTLS" do |resp|
state.tls? and return resp.fail_bad_args "TLS already established"
state.not_authenticated? or return resp.fail_bad_state(state)
resp.done_ok
state.use_tls
end
on "LOGIN" do |resp|
state.not_authenticated? or return resp.fail_bad_state(state)
args = resp.command.args
args.count == 2 or return resp.fail_bad_args
username, password = args
username == config.user[:username] or return resp.fail_no "wrong username"
password == config.user[:password] or return resp.fail_no "wrong password"
state.authenticate config.user
resp.done_ok
end
on "AUTHENTICATE" do |resp|
state.not_authenticated? or return resp.fail_bad_state(state)
args = resp.command.args
(1..2) === args.length or return resp.fail_bad_args
args.first == "PLAIN" or return resp.fail_no "unsupported"
if args.length == 2
response_b64 = args.last
else
response_b64 = resp.request_continuation("") || ""
state.commands << {continuation: response_b64}
end
response_b64.strip == ?* and return resp.fail_bad "canceled"
response = Base64.decode64(response_b64) rescue :decode64_failed
response == :decode64_failed and return resp.fail_bad "invalid b64"
# TODO: support mechanisms other than PLAIN.
parts = response.split("\0")
parts.length == 3 or return resp.fail_bad "invalid"
authzid, authcid, password = parts
authzid = authcid if authzid.empty?
authzid == config.user[:username] or return resp.fail_no "wrong username"
authcid == config.user[:username] or return resp.fail_no "wrong username"
password == config.user[:password] or return resp.fail_no "wrong password"
state.authenticate config.user
resp.done_ok
end
on "ENABLE" do |resp|
state.authenticated? or return resp.fail_bad_state(state)
resp.args&.any? or return resp.fail_bad_args
enabled = (resp.args & config.capabilities_enablable) - state.enabled
state.enabled.concat enabled
resp.untagged :ENABLED, enabled
resp.done_ok
end
# Will be used as defaults for mailboxes that haven't set their own values
RFC3501_6_3_1_SELECT_EXAMPLE_DATA = {
exists: 172,
recent: 1,
unseen: 12,
uidvalidity: 3857529045,
uidnext: 4392,
flags: %i[Answered Flagged Deleted Seen Draft].freeze,
permanentflags: %i[Deleted Seen *].freeze,
}.freeze
def select_handler(command, resp)
state.user or return resp.fail_bad_state(state)
name, args = resp.args
name or return resp.fail_bad_args
name = name.upcase if name.to_s.casecmp? "inbox"
mbox = config.mailboxes[name]
mbox or return resp.fail_no "invalid mailbox %p" % [name]
state.select mbox: mbox, args: args
attrs = RFC3501_6_3_1_SELECT_EXAMPLE_DATA.merge mbox.to_h
resp.untagged "%{exists} EXISTS" % attrs
resp.untagged "%{recent} RECENT" % attrs
resp.untagged "OK [UNSEEN %{unseen}] ..." % attrs
resp.untagged "OK [UIDVALIDITY %{uidvalidity}] UIDs valid" % attrs
resp.untagged "OK [UIDNEXT %{uidnext}] Predicted next UID" % attrs
if mbox[:uidnotsticky]
resp.untagged "NO [UIDNOTSTICKY] Non-persistent UIDs"
end
resp.untagged "FLAGS (%s)" % [flags(attrs[:flags])]
resp.untagged "OK [PERMANENTFLAGS (%s)] Limited" % [
flags(attrs[:permanentflags])
]
code = command == "SELECT" ? "READ-WRITE" : "READ-ONLY"
resp.done_ok code: code
end
on "SELECT" do |resp| select_handler "SELECT", resp end
on "EXAMINE" do |resp| select_handler "EXAMINE", resp end
on "CLOSE", "UNSELECT" do |resp|
resp.args.nil? or return resp.fail_bad_args
state.unselect
resp.done_ok
end
private
attr_reader :config, :state
def flags(flags)
flags.map { [Symbol === _1 ? "\\" : "", _1].join }.join(" ")
end
end
end