remailer 0.4.21 → 0.5.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.
Files changed (31) hide show
  1. data/README.rdoc +25 -19
  2. data/VERSION +1 -1
  3. data/lib/remailer.rb +7 -1
  4. data/lib/remailer/{connection.rb → abstract_connection.rb} +63 -167
  5. data/lib/remailer/constants.rb +10 -0
  6. data/lib/remailer/email_address.rb +41 -0
  7. data/lib/remailer/imap.rb +6 -0
  8. data/lib/remailer/imap/client.rb +228 -0
  9. data/lib/remailer/imap/client/interpreter.rb +101 -0
  10. data/lib/remailer/imap/server.rb +15 -0
  11. data/lib/remailer/imap/server/interpreter.rb +2 -0
  12. data/lib/remailer/interpreter.rb +7 -6
  13. data/lib/remailer/interpreter/state_proxy.rb +20 -2
  14. data/lib/remailer/smtp.rb +6 -0
  15. data/lib/remailer/smtp/client.rb +329 -0
  16. data/lib/remailer/{connection/smtp_interpreter.rb → smtp/client/interpreter.rb} +4 -4
  17. data/lib/remailer/smtp/server.rb +130 -0
  18. data/lib/remailer/smtp/server/interpreter.rb +237 -0
  19. data/lib/remailer/smtp/server/transaction.rb +29 -0
  20. data/lib/remailer/socks5.rb +5 -0
  21. data/lib/remailer/socks5/client.rb +5 -0
  22. data/lib/remailer/{connection/socks5_interpreter.rb → socks5/client/interpreter.rb} +3 -2
  23. data/lib/remailer/support.rb +5 -0
  24. data/remailer.gemspec +27 -9
  25. data/test/unit/remailer_imap_client_interpreter_test.rb +14 -0
  26. data/test/unit/remailer_imap_client_test.rb +125 -0
  27. data/test/unit/{remailer_connection_smtp_interpreter_test.rb → remailer_smtp_client_interpreter_test.rb} +33 -33
  28. data/test/unit/{remailer_connection_test.rb → remailer_smtp_client_test.rb} +11 -11
  29. data/test/unit/remailer_smtp_server_test.rb +83 -0
  30. data/test/unit/{remailer_connection_socks5_interpreter_test.rb → remailer_socks5_client_interpreter_test.rb} +25 -17
  31. metadata +29 -11
@@ -0,0 +1,10 @@
1
+ module Remailer::Constants
2
+ # == Constants ============================================================
3
+
4
+ LINE_REGEXP = /^.*?\r?\n/.freeze
5
+ CRLF = "\r\n".freeze
6
+
7
+ SMTP_PORT = 25
8
+ IMAPS_PORT = 993
9
+ SOCKS5_PORT = 1080
10
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: BINARY
2
+ #
3
+ # RFC822 Email Address Regex
4
+ # --------------------------
5
+ #
6
+ # Originally written by Cal Henderson
7
+ # c.f. http://iamcal.com/publish/articles/php/parsing_email/
8
+ #
9
+ # Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
10
+ #
11
+ # Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
12
+ # http://creativecommons.org/licenses/by-sa/2.5/
13
+ #
14
+
15
+ class Remailer::EmailAddress
16
+ # == Class Methods ========================================================
17
+
18
+ PATTERN =
19
+ begin
20
+ qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
21
+ dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
22
+ atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
23
+ '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
24
+ quoted_pair = '\\x5c[\\x00-\\x7f]'
25
+ domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
26
+ quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
27
+ domain_ref = atom
28
+ sub_domain = "(?:#{domain_ref}|#{domain_literal})"
29
+ word = "(?:#{atom}|#{quoted_string})"
30
+ domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
31
+ local_part = "#{word}(?:\\x2e#{word})*"
32
+ addr_spec = "#{local_part}\\x40#{domain}"
33
+ pattern = /\A#{addr_spec}\z/
34
+ end
35
+
36
+ # == Class Methods ========================================================
37
+
38
+ def self.valid?(address)
39
+ PATTERN.match(address)
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ module Remailer::IMAP
2
+ # == Submodules ===========================================================
3
+
4
+ autoload(:Client, 'remailer/imap/client')
5
+ autoload(:Server, 'remailer/imap/server')
6
+ end
@@ -0,0 +1,228 @@
1
+ class Remailer::IMAP::Client < Remailer::AbstractConnection
2
+ # == Exceptions ===========================================================
3
+
4
+ # == Extensions ===========================================================
5
+
6
+ include Remailer::Support
7
+
8
+ # == Submodules ===========================================================
9
+
10
+ autoload(:Interpreter, 'remailer/imap/client/interpreter')
11
+
12
+ # == Constants ============================================================
13
+
14
+ DEFAUT_TIMEOUT = 60
15
+
16
+ # == Properties ===========================================================
17
+
18
+ # == Class Methods ========================================================
19
+
20
+ def self.default_timeout
21
+ DEFAULT_TIMEOUT
22
+ end
23
+
24
+ def self.default_port
25
+ IMAPS_PORT
26
+ end
27
+
28
+ # Opens a connection to a specific IMAP server. Options can be specified:
29
+ # * port => Numerical port number (default is 993)
30
+ # * require_tls => If true will fail connections to non-TLS capable
31
+ # servers (default is false)
32
+ # * username => Username to authenticate with the IMAP server
33
+ # * password => Password to authenticate with the IMAP server
34
+ # * use_tls => Will use TLS if availble (default is true)
35
+ # * debug => Where to send debugging output (IO or Proc)
36
+ # * connect => Where to send a connection notification (IO or Proc)
37
+ # * error => Where to send errors (IO or Proc)
38
+ # * on_connect => Called upon successful connection (Proc)
39
+ # * on_error => Called upon connection error (Proc)
40
+ # * on_disconnect => Called when connection is closed (Proc)
41
+ # A block can be supplied in which case it will stand in as the :connect
42
+ # option. The block will recieve a first argument that is the status of
43
+ # the connection, and an optional second that is a diagnostic message.
44
+ def self.open(imap_server, options = nil, &block)
45
+ super(imap_server, options, &block)
46
+ end
47
+
48
+ # == Instance Methods =====================================================
49
+
50
+ def after_initialize
51
+ if (using_proxy?)
52
+ @connecting_to_proxy = true
53
+ use_socks5_interpreter!
54
+ else
55
+ if (@options[:use_tls])
56
+ self.start_tls
57
+ end
58
+
59
+ use_imap_interpreter!
60
+ end
61
+
62
+ @command_tags = { }
63
+ @issued_command = { }
64
+ end
65
+
66
+ # Callback receiver for when the proxy connection has been completed.
67
+ def after_proxy_connected
68
+ if (@options[:use_tls])
69
+ self.start_tls
70
+ end
71
+
72
+ use_imap_interpreter!
73
+ end
74
+
75
+ def after_unbind
76
+ debug_notification(:disconnect, "Disconnected by remote.")
77
+
78
+ @command_tags.each do |tag, callback|
79
+ callback[1].call(nil)
80
+ end
81
+ end
82
+
83
+ def receive_response(tag, status, message, additional = nil)
84
+ if (set = @command_tags.delete(tag))
85
+ @issued_command.delete(set[0])
86
+ set[1].call(status, message, additional)
87
+ end
88
+ end
89
+
90
+ # -- Commands -------------------------------------------------------------
91
+
92
+ def capability
93
+ self.issue_command('CAPABILITY') do |status, message, additional|
94
+ yield(additional)
95
+ end
96
+ end
97
+
98
+ def login(username, password)
99
+ self.issue_command('LOGIN', quoted(username), quoted(password)) do |status, message, additional|
100
+ yield(status, message, additional)
101
+ end
102
+ end
103
+
104
+ def list(reference_name = '/', mailbox_name = '*')
105
+ self.issue_command('LIST', quoted(reference_name), quoted(mailbox_name)) do |status, message, additional|
106
+ yield(status, message, additional)
107
+ end
108
+ end
109
+
110
+ def select(mailbox_name = 'INBOX')
111
+ self.issue_command('SELECT', quoted(mailbox_name)) do |status, message, additional|
112
+ yield(status, message, additional)
113
+ end
114
+ end
115
+
116
+ def examine(mailbox_name = 'INBOX')
117
+ self.issue_command('EXAMINE', quoted(mailbox_name)) do |status, message, additional|
118
+ yield(status, message, additional)
119
+ end
120
+ end
121
+
122
+ def status(mailbox_name = 'INBOX', *flags)
123
+ flags.flatten!
124
+ flags = [ :uidnext, :messages ] if (flags.empty?)
125
+
126
+ # Valid flags are: MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN
127
+
128
+ self.issue_command('STATUS', quoted(mailbox_name), "(#{flags.collect { |f| f.to_s.upcase }.join(' ')})") do |status, message, additional|
129
+ yield(status, message, additional)
130
+ end
131
+ end
132
+
133
+ def noop
134
+ self.issue_command('NOOP') do |status, message, additional|
135
+ yield(status, message, additional)
136
+ end
137
+ end
138
+
139
+ def fetch(range, *options)
140
+ fetch_options = { }
141
+
142
+ options.each do |option|
143
+ case (option)
144
+ when Hash
145
+ fetch_options.merge(option)
146
+ else
147
+ fetch_options[option] = true
148
+ end
149
+ end
150
+
151
+ if (fetch_options.empty?)
152
+ fetch_options[:all] = true
153
+ end
154
+
155
+ sequence_set =
156
+ case (range)
157
+ when Range
158
+ "#{range.min}:#{range.max}"
159
+ else
160
+ range.to_s
161
+ end
162
+
163
+ items =
164
+ if (fetch_options[:all])
165
+ 'ALL'
166
+ elsif (fetch_options[:fast])
167
+ 'FAST'
168
+ elsif (fetch_options[:full])
169
+ 'FULL'
170
+ elsif (body_options = fetch_options[:body])
171
+ case (body_options)
172
+ when Hash
173
+ # ...
174
+ else
175
+ 'BODY'
176
+ end
177
+ end
178
+
179
+ self.issue_command('FETCH', sequence_set, items) do |status, message, additional|
180
+ yield(status, message, additional)
181
+ end
182
+ end
183
+
184
+ def idle
185
+ self.issue_command('IDLE') do |status, message, additional|
186
+ yield(status, message, additional)
187
+ end
188
+
189
+ lambda { self.send_line('DONE') }
190
+ end
191
+
192
+ protected
193
+ # Switches to use the IMAP interpreter for all subsequent communication
194
+ def use_imap_interpreter!
195
+ @interpreter = Interpreter.new(:delegate => self)
196
+ end
197
+
198
+ def next_tag
199
+ @next_tag ||= (rand(1 << 32) << 32) | (1 << 64)
200
+
201
+ @next_tag += 1
202
+
203
+ @next_tag.to_s(16)
204
+ end
205
+
206
+ def issue_command(*args, &block)
207
+ tag = self.next_tag
208
+
209
+ self.send_line(([ tag ] + args.flatten).join(' '))
210
+
211
+ @command_tags[tag] = [ args.first, block ]
212
+ @issued_command[args.first] = [ tag, block ]
213
+
214
+ tag
215
+ end
216
+
217
+ def pending_command_with_tag(tag)
218
+ set = @command_tags[tag]
219
+
220
+ set and set[0] or false
221
+ end
222
+
223
+ def pending_command_of_type(type)
224
+ set = @issued_command[tag]
225
+
226
+ set and set[0] or false
227
+ end
228
+ end
@@ -0,0 +1,101 @@
1
+ class Remailer::IMAP::Client::Interpreter < Remailer::Interpreter
2
+ # == Constants ============================================================
3
+
4
+ LINE_REGEXP = /^(.*?)\r?\n/.freeze
5
+ RESPONSE_REGEXP = /^(\S+)(?:\s+(?:(OK|NO|BAD)\s+)?([^\r\n]*))?\r?\n/.freeze
6
+
7
+ # == Class Methods ========================================================
8
+
9
+ def self.split_reply(reply)
10
+ if (m = reply.match(RESPONSE_REGEXP))
11
+ parts = m.to_a
12
+ parts.shift
13
+
14
+ parts
15
+ else
16
+ nil
17
+ end
18
+ end
19
+
20
+ def self.split_list_definition(string)
21
+ m = string.match(/^\(([^\)]+)\)\s+\"((?:\\\"|\\x[0-9a-fA-F][0-9a-fA-F]|\\[abtnvfr]|.)+)\"\s+\"((?:\\\"|\\x[0-9a-fA-F][0-9a-fA-F]|\\[abtnvfr]|.)+)\"/)
22
+
23
+ return unless (m)
24
+
25
+ split = m.to_a
26
+ split.shift
27
+ split[0] = split[0].split(/\s+/)
28
+
29
+ split
30
+ end
31
+
32
+ # == State Definitions ====================================================
33
+
34
+ # Based on the RFC3501 specification with extensions
35
+
36
+ parse(LINE_REGEXP) do |data|
37
+ split_reply(data)
38
+ end
39
+
40
+ state :initialized do
41
+ interpret('*') do |tag, status, message|
42
+ delegate.connect_notification(status, message)
43
+
44
+ enter_state(:connected)
45
+ end
46
+ end
47
+
48
+ state :connected do
49
+ interpret('*') do |tag, status, message|
50
+ message_parts = message.split(/\s+/)
51
+ message_key = message_parts.shift
52
+
53
+ case (message_key)
54
+ when 'CAPABILITY'
55
+ @additional ||= [ ]
56
+ @additional += message_parts
57
+ when 'LIST'
58
+ message = message.sub(/^LIST\s+/, '')
59
+
60
+ if (split = self.class.split_list_definition(message))
61
+ @additional ||= [ ]
62
+ @additional << split.reverse
63
+ end
64
+ end
65
+ end
66
+
67
+ default do |tag, status, message|
68
+ delegate.receive_response(tag, status, message, @additional)
69
+
70
+ @additional = nil
71
+ end
72
+ end
73
+
74
+ state :fetch do
75
+ parse(LINE_REGEXP)
76
+
77
+ enter do
78
+ @additional = ''
79
+ end
80
+
81
+ interpret(/^\*\s+(\d+)\s+FETCH\s+(.*)/) do |uid, line|
82
+ @additional << line
83
+ end
84
+
85
+ interpret(/^(\w+)\s+(OK|NO|BAD)\s+(FETCH\s+.*)/) do |tag, status, message|
86
+ delegate.receive_response(tag, [ status, message ], @additional)
87
+
88
+ @additional = nil
89
+ end
90
+
91
+ default do |line|
92
+ @additional << line
93
+ end
94
+ end
95
+
96
+ # == Instance Methods =====================================================
97
+
98
+ def label
99
+ 'IMAP'
100
+ end
101
+ end
@@ -0,0 +1,15 @@
1
+ require 'socket'
2
+ require 'eventmachine'
3
+
4
+ class Remailer::IMAP::Server < EventMachine::Protocols::LineAndTextProtocol
5
+ # == Submodules ===========================================================
6
+
7
+ autoload(:Interpreter, 'remailer/imap/server/imap_interpreter')
8
+
9
+ # == Constants ============================================================
10
+
11
+ # == Class Methods ========================================================
12
+
13
+ # == Instance Methods =====================================================
14
+
15
+ end
@@ -0,0 +1,2 @@
1
+ class Remailer::IMAP::Server::Interpreter < Remailer::Interpreter
2
+ end
@@ -67,9 +67,9 @@ class Remailer::Interpreter
67
67
 
68
68
  # This is a method to convert a spec and a block into a proper parser
69
69
  # method. If spec is specified, it should be a Fixnum, or a Regexp. A
70
- # Fixnum defines a minimum size to process, useful for packed binary
71
- # streams, and a Regexp defines a pattern that must match before the parser
72
- # is engaged.
70
+ # Fixnum defines a minimum size to process, useful for packed binary
71
+ # streams, while a Regexp defines a pattern that must match before the
72
+ # parser is engaged.
73
73
  def self.create_parser_for_spec(spec, &block)
74
74
  case (spec)
75
75
  when nil
@@ -202,17 +202,18 @@ class Remailer::Interpreter
202
202
  end
203
203
 
204
204
  # Processes a given input string into interpretable tokens, processes these
205
- # tokens, and removes them from the input string. If no interpretable
206
- # tokens could be found, returns immediately. An optional block can be
205
+ # tokens, and removes them from the input string. An optional block can be
207
206
  # given that will be called as each interpretable token is discovered with
208
207
  # the token provided as the argument.
209
208
  def process(s)
210
209
  _parser = parser
211
210
 
212
- while (parsed = s.empty? ? false : instance_exec(s, &_parser))
211
+ while (parsed = instance_exec(s, &_parser))
213
212
  yield(parsed) if (block_given?)
214
213
 
215
214
  interpret(*parsed)
215
+
216
+ break if (s.empty?)
216
217
  end
217
218
  end
218
219