ruby-net-nntp 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +12 -1
- data/lib/net/nntp.rb +345 -336
- data/lib/net/nntp/version.rb +1 -1
- data/lib/net/nntp_article.rb +22 -22
- data/lib/net/nntp_group.rb +61 -61
- data/test/functional/test_nntp.rb +229 -229
- data/test/mock/mock_socket.rb +213 -213
- data/test/unit/test_nntp_group.rb +1 -1
- metadata +3 -3
data/CHANGELOG
CHANGED
@@ -1,4 +1,15 @@
|
|
1
|
-
2007-07-02
|
1
|
+
2007-07-02 17:15 +0200 Anton 'tony' Bangratz <anton.bangratz@gmail.com> (6fe65b246b19 [tip])
|
2
|
+
|
3
|
+
* lib/net/nntp/version.rb:
|
4
|
+
new version; new release
|
5
|
+
|
6
|
+
2007-07-02 17:14 +0200 Anton 'tony' Bangratz <anton.bangratz@gmail.com> (0ec007283eb8)
|
7
|
+
|
8
|
+
* lib/net/nntp.rb:
|
9
|
+
bugfixes: read_response response detection only on first line,
|
10
|
+
listgroup accepting no group (error if none selected)
|
11
|
+
|
12
|
+
2007-07-02 16:25 +0200 Anton 'tony' Bangratz <anton.bangratz@gmail.com> (a75e65c0e31c)
|
2
13
|
|
3
14
|
* lib/net/nntp.rb, lib/net/nntp/version.rb:
|
4
15
|
xover and log bugfix
|
data/lib/net/nntp.rb
CHANGED
@@ -11,341 +11,350 @@ require 'log4r'
|
|
11
11
|
|
12
12
|
|
13
13
|
module Net
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
14
|
+
class NNTP
|
15
|
+
include Timeout # :nodoc:
|
16
|
+
|
17
|
+
# Statuses of one-line responses
|
18
|
+
ONELINE_STATUSES = %w( 111 200 201 205 223 235 240 281 335 340 381 400 401 403 411 412 420 421 422 423 430 435 436 437 440 441 480 483 500 501 502 503 504 ).freeze
|
19
|
+
|
20
|
+
# Statuses of multiline responses
|
21
|
+
MULTILINE_STATUSES = %w( 100 101 215 220 221 222 224 225 230 231 ).freeze
|
22
|
+
|
23
|
+
# Error to indicate that NNTP Command failed gracefully
|
24
|
+
class CommandFailedError < StandardError
|
25
|
+
end
|
26
|
+
# Error to indicate that a Protocol Error occured during NNTP command execution
|
27
|
+
class ProtocolError < StandardError
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
attr_reader :socket, :grouplist, :overview_format
|
32
|
+
|
33
|
+
def self.logger=(logger)
|
34
|
+
@@logger = logger
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.logger
|
38
|
+
@@logger
|
39
|
+
end
|
40
|
+
|
41
|
+
def logger
|
42
|
+
@@logger
|
43
|
+
end
|
44
|
+
def debug(message)
|
45
|
+
@@logger.debug(message)
|
46
|
+
end
|
47
|
+
|
48
|
+
# initializes NNTP class with host and port
|
49
|
+
def initialize(host, port = 119)
|
50
|
+
@host = host
|
51
|
+
@port = port
|
52
|
+
@socket_class = TCPSocket
|
53
|
+
@group = nil
|
54
|
+
@@logger ||= Log4r::Logger['net::nntp'] || Log4r::Logger.new('net::nntp')
|
55
|
+
end
|
56
|
+
|
57
|
+
# Actually connects to NNTP host and port given in new()
|
58
|
+
def connect
|
59
|
+
@socket = @socket_class.new(@host, @port)
|
60
|
+
@welcome = read_response()
|
61
|
+
debug "Welcome: #{@welcome[0]} "
|
62
|
+
@welcome
|
63
|
+
end
|
64
|
+
|
65
|
+
# Closes connection. If not reconnected, subsequent calls of commands raise exceptions
|
66
|
+
def close
|
67
|
+
debug 'closing connection per request'
|
68
|
+
@socket.close unless socket.closed?
|
69
|
+
end
|
70
|
+
|
71
|
+
def has_xover?
|
72
|
+
!help.select {|i| i =~ /\bxover\b/i}.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
def has_over?
|
76
|
+
!help.select {|i| i =~ /\bover\b/i}.empty?
|
77
|
+
end
|
78
|
+
# Uses authinfo commands to authenticate. Timeout for first command is set to 10 seconds.
|
79
|
+
#
|
80
|
+
# Returns true on success, false on failure.
|
81
|
+
def authenticate(user, pass)
|
82
|
+
cmd = "authinfo user #{user}"
|
83
|
+
debug "Authenticating: Sending #{cmd}"
|
84
|
+
send_cmd cmd
|
85
|
+
response_array = read_response()
|
86
|
+
response = response_array[0]
|
87
|
+
debug "Authenticating: Response #{response}"
|
88
|
+
if response[0..2] == '381' then
|
89
|
+
cmd = "authinfo pass #{pass}"
|
90
|
+
debug "Authenticating: Sending #{cmd}"
|
91
|
+
send_cmd cmd
|
92
|
+
response_array = read_response()
|
93
|
+
response = response_array[0]
|
94
|
+
debug "Authenticating: Response #{response}"
|
95
|
+
end
|
96
|
+
return response && response[0..2] == '281'
|
97
|
+
end
|
98
|
+
|
99
|
+
# Issues 'GROUP' command to NNTP Server and creates new active group from returning data.
|
100
|
+
#
|
101
|
+
# Throws CommandFailedError
|
102
|
+
#
|
103
|
+
def group(group)
|
104
|
+
send_cmd "group #{group}"
|
105
|
+
response = read_response(true)
|
106
|
+
responsecode, cnt, first, last, name = response[0].split
|
107
|
+
if responsecode == '211'
|
108
|
+
@group = Group.new(name)
|
109
|
+
@group.article_info = [cnt, first, last]
|
110
|
+
@group
|
111
|
+
else
|
112
|
+
raise CommandFailedError, response[0]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Issues 'HELP' command to NNTP Server, and returns raw response.
|
117
|
+
def help
|
118
|
+
send_cmd("help")
|
119
|
+
read_response()
|
120
|
+
end
|
121
|
+
|
122
|
+
# Issues 'XHDR' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
|
123
|
+
# otherwise.
|
124
|
+
#
|
125
|
+
# TODO:: Implement XHDR header <message-id>
|
126
|
+
def xhdr(header, groupname=nil, rest=nil)
|
127
|
+
raise CommandFailedError, 'No group selected' unless @group || groupname
|
128
|
+
if @group.nil?
|
129
|
+
group(groupname)
|
130
|
+
elsif groupname.nil?
|
131
|
+
else
|
132
|
+
@group.name != groupname
|
133
|
+
group(groupname)
|
134
|
+
end
|
135
|
+
cmd = "xhdr #{header}"
|
136
|
+
suffix = numbers_or_id(rest)
|
137
|
+
send_cmd([cmd, suffix].join(' '))
|
138
|
+
read_response()
|
139
|
+
end
|
140
|
+
|
141
|
+
# Issues 'XOVER' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
|
142
|
+
# otherwise. If no group is selected nor given, raises error.
|
143
|
+
# Parameter 'rest' can be in the form of :from => number, :to => number or :messageid => 'messageid',
|
144
|
+
# if not set, a 'next' command is issued to the server prior to the xover command
|
145
|
+
#
|
146
|
+
def xover(groupname=nil, rest=nil)
|
147
|
+
raise CommandFailedError, 'No group selected' unless @group || groupname
|
148
|
+
if @group.nil?
|
149
|
+
group(groupname)
|
150
|
+
elsif groupname.nil?
|
151
|
+
else
|
152
|
+
@group.name != groupname
|
153
|
+
group(groupname)
|
154
|
+
end
|
155
|
+
debug "Selected Group: #{@group.name}"
|
156
|
+
self.next unless rest
|
157
|
+
prefix = "xover"
|
158
|
+
suffix = numbers_or_id(rest)
|
159
|
+
cmd = [prefix, suffix].join ' '
|
160
|
+
send_cmd(cmd)
|
161
|
+
response = nil
|
162
|
+
timeout(10) do
|
163
|
+
response = read_response()
|
164
|
+
end
|
165
|
+
response
|
166
|
+
end
|
167
|
+
|
168
|
+
# Issues 'LISTGROUP' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
|
169
|
+
# otherwise.
|
170
|
+
#
|
171
|
+
def listgroup(groupname=nil)
|
172
|
+
raise CommandFailedError, 'No group selected' unless @group || groupname
|
173
|
+
if @group.nil?
|
174
|
+
group(groupname)
|
175
|
+
elsif @group.name != groupname
|
176
|
+
group(groupname)
|
177
|
+
end
|
178
|
+
debug "Selected Group: #{@group.name}"
|
179
|
+
send_cmd('listgroup')
|
180
|
+
read_response()
|
181
|
+
end
|
182
|
+
|
183
|
+
# Issues 'LIST' command to NNTP Server. Depending on server capabilities and given keyword, can either set overview
|
184
|
+
# format (if called with 'overview.fmt') or create a grouplist (see Attributes)
|
185
|
+
#
|
186
|
+
# Throws CommandFailedError
|
187
|
+
|
188
|
+
def list(keyword=nil, pattern=nil)
|
189
|
+
cmd = ['list', keyword, pattern].join ' '
|
190
|
+
send_cmd(cmd)
|
191
|
+
list = read_response()
|
192
|
+
responsecode = list[0][0..2]
|
193
|
+
case responsecode
|
194
|
+
when '215'
|
195
|
+
case keyword
|
196
|
+
when /overview.fmt/
|
197
|
+
@overview_format_raw = list
|
198
|
+
@overview_format = Net::NNTP.parse_overview_format list.join
|
199
|
+
else
|
200
|
+
create_grouplist(list)
|
201
|
+
list
|
202
|
+
end
|
203
|
+
|
204
|
+
when '501', '503', '500'
|
205
|
+
raise CommandFailedError, list
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# prepares overview format as read from server, used by Net::NNTP::Article and list()
|
210
|
+
def self.parse_overview_format(format)
|
211
|
+
overview_format = %w{id}
|
212
|
+
format.split(/\r?\n/).each do |line|
|
213
|
+
next if line[0] == ?2 || line[0] == ?.
|
214
|
+
ident = line.scan(/\w/).join.downcase
|
215
|
+
unless ident[0..3] == 'xref'
|
216
|
+
overview_format << ident
|
217
|
+
else
|
218
|
+
overview_format << 'xref'
|
219
|
+
end
|
220
|
+
end
|
221
|
+
overview_format
|
222
|
+
end
|
223
|
+
|
224
|
+
# TODO: complete implementation
|
225
|
+
def stat
|
226
|
+
end
|
227
|
+
|
228
|
+
# Issues 'HEAD' command to NNTP server, returning raw response
|
229
|
+
#
|
230
|
+
# Throws CommandFailedError
|
231
|
+
def head(args=nil)
|
232
|
+
suffix = numbers_or_id(args)
|
233
|
+
cmd = 'head'
|
234
|
+
cmd = ['head', suffix].join " " if suffix
|
235
|
+
send_cmd(cmd)
|
236
|
+
response = read_response()
|
237
|
+
case response[0][0..2]
|
238
|
+
when '221'
|
239
|
+
return response
|
240
|
+
else
|
241
|
+
raise CommandFailedError, response
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Issues 'BODY' command to NNTP server, returning raw response.
|
246
|
+
# options:: messageid|id
|
247
|
+
#
|
248
|
+
# Throws CommandFailedError
|
249
|
+
def body(args=nil)
|
250
|
+
suffix = args
|
251
|
+
cmd = 'body'
|
252
|
+
cmd = ['body', suffix].join " " if suffix
|
253
|
+
send_cmd(cmd)
|
254
|
+
response = read_response()
|
255
|
+
case response[0][0..2]
|
256
|
+
when '222'
|
257
|
+
return response
|
258
|
+
else
|
259
|
+
raise CommandFailedError, response
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Issues 'ARTICLE' command to NNTP server, returning raw response.
|
264
|
+
# options:: messageid|id
|
265
|
+
#
|
266
|
+
# Throws CommandFailedError
|
267
|
+
def article(args=nil)
|
268
|
+
suffix = args
|
269
|
+
cmd = 'article'
|
270
|
+
cmd = ['article', suffix].join " " if suffix
|
271
|
+
send_cmd(cmd)
|
272
|
+
response = read_response()
|
273
|
+
case response[0][0..2]
|
274
|
+
when '220'
|
275
|
+
return response
|
276
|
+
else
|
277
|
+
raise CommandFailedError, response
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def last_or_next(cmd)
|
282
|
+
raise ProtocolError, "No group selected" unless @group
|
283
|
+
send_cmd(cmd)
|
284
|
+
response = read_response()[0]
|
285
|
+
code = response[0..2]
|
286
|
+
article = @group.articles.create
|
287
|
+
case code
|
288
|
+
when '223'
|
289
|
+
code, id, msgid, what = response.split
|
290
|
+
article.id = id
|
291
|
+
article.messageid = msgid
|
292
|
+
else
|
293
|
+
raise CommandFailedError, response
|
294
|
+
end
|
295
|
+
response
|
296
|
+
end
|
297
|
+
|
298
|
+
# Issues the LAST command to the NNTP server, returning the raw response
|
299
|
+
def last
|
300
|
+
last_or_next("last")
|
301
|
+
end
|
302
|
+
|
303
|
+
def next
|
304
|
+
last_or_next("next")
|
305
|
+
end
|
306
|
+
|
307
|
+
def create_grouplist(response)
|
308
|
+
@grouplist = {}
|
309
|
+
response.each_with_index do |line, idx|
|
310
|
+
next if idx == 0
|
311
|
+
break if line =~ /^\.$/
|
312
|
+
groupinfo = line.split
|
313
|
+
group = Group.new groupinfo.shift
|
314
|
+
group.article_info = groupinfo
|
315
|
+
@grouplist[group.name] = group
|
316
|
+
end
|
317
|
+
@grouplist
|
318
|
+
end
|
319
|
+
|
320
|
+
def send_cmd(cmd)
|
321
|
+
debug "Sending: '#{cmd}'"
|
322
|
+
@socket.write(cmd+"\r\n")
|
323
|
+
end
|
324
|
+
|
325
|
+
def read_response(force_oneline=false)
|
326
|
+
response = ''
|
327
|
+
str = ''
|
328
|
+
ra = []
|
329
|
+
linecnt = 0
|
330
|
+
loop do
|
331
|
+
str = @socket.readline
|
332
|
+
ra << str.chomp if str
|
333
|
+
break if force_oneline || (str == "." || !str || (linecnt == 0 && ONELINE_STATUSES.include?(str[0..2])) )
|
334
|
+
linecnt += 1
|
335
|
+
end
|
336
|
+
debug "Response: '#{ra}'"
|
337
|
+
ra
|
338
|
+
end
|
339
|
+
|
340
|
+
def numbers_or_id(hash)
|
341
|
+
return nil unless hash
|
342
|
+
suffix = ''
|
343
|
+
from = hash[:from]
|
344
|
+
to = hash[:to]
|
345
|
+
msgid = hash[:message_id]
|
346
|
+
if from
|
347
|
+
suffix = "#{from}-"
|
348
|
+
suffix += "#{to}" if to
|
349
|
+
elsif msgid
|
350
|
+
suffix = "#{msgid}"
|
351
|
+
end
|
352
|
+
suffix
|
353
|
+
end
|
354
|
+
|
355
|
+
private :read_response, :numbers_or_id, :send_cmd, :last_or_next, :create_grouplist
|
356
|
+
|
357
|
+
end
|
349
358
|
end
|
350
359
|
|
351
|
-
# vim:sts=2:ts=2:sw=2:sta:
|
360
|
+
# vim:sts=2:ts=2:sw=2:sta:et
|