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 CHANGED
@@ -1,4 +1,15 @@
1
- 2007-07-02 16:25 +0200 Anton 'tony' Bangratz <anton.bangratz@gmail.com> (a75e65c0e31c [tip])
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
- class NNTP
15
- include Timeout # :nodoc:
16
-
17
- # Statuses of one-line responses
18
- ONELINE_STATUSES = %w( 111 200 201 223 281 381 411 412 422 480 500 501 ).freeze
19
-
20
- # Statuses of multiline responses
21
- MULTILINE_STATUSES = %w( 100 215 220 221 222 224 ).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(groupname, header, rest)
127
- group(groupname) unless (@group && @group.name == groupname)
128
- cmd = "xhdr #{header}"
129
- suffix = numbers_or_id(rest)
130
- send_cmd([cmd, suffix].join(' '))
131
- read_response()
132
- end
133
-
134
- # Issues 'XOVER' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
135
- # otherwise. If no group is selected nor given, raises error.
136
- # Parameter 'rest' can be in the form of :from => number, :to => number or :messageid => 'messageid',
137
- # if not set, a 'next' command is issued to the server prior to the xover command
138
- #
139
- def xover(groupname=nil, rest=nil)
140
- raise CommandFailedError, 'No group selected' unless @group || groupname
141
- if @group.nil?
142
- group(groupname)
143
- elsif @group.name != groupname
144
- group(groupname)
145
- end
146
- debug "Selected Group: #{@group.name}"
147
- self.next unless rest
148
- prefix = "xover"
149
- suffix = numbers_or_id(rest)
150
- cmd = [prefix, suffix].join ' '
151
- send_cmd(cmd)
152
- response = nil
153
- timeout(10) do
154
- response = read_response()
155
- end
156
- response
157
- end
158
-
159
- # Issues 'LISTGROUP' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
160
- # otherwise.
161
- #
162
- def listgroup(groupname=nil)
163
- raise CommandFailedError, 'No group selected' unless @group || groupname
164
- if @group.nil?
165
- group(groupname)
166
- elsif @group.name != groupname
167
- group(groupname)
168
- end
169
- debug "Selected Group: #{@group.name}"
170
- send_cmd('listgroup')
171
- read_response()
172
- end
173
-
174
- # Issues 'LIST' command to NNTP Server. Depending on server capabilities and given keyword, can either set overview
175
- # format (if called with 'overview.fmt') or create a grouplist (see Attributes)
176
- #
177
- # Throws CommandFailedError
178
-
179
- def list(keyword=nil, pattern=nil)
180
- cmd = ['list', keyword, pattern].join ' '
181
- send_cmd(cmd)
182
- list = read_response()
183
- responsecode = list[0][0..2]
184
- case responsecode
185
- when '215'
186
- case keyword
187
- when /overview.fmt/
188
- @overview_format_raw = list
189
- @overview_format = Net::NNTP.parse_overview_format list.join
190
- else
191
- create_grouplist(list)
192
- list
193
- end
194
-
195
- when '501', '503', '500'
196
- raise CommandFailedError, list
197
- end
198
- end
199
-
200
- # prepares overview format as read from server, used by Net::NNTP::Article and list()
201
- def self.parse_overview_format(format)
202
- overview_format = %w{id}
203
- format.split(/\r?\n/).each do |line|
204
- next if line[0] == ?2 || line[0] == ?.
205
- ident = line.scan(/\w/).join.downcase
206
- unless ident[0..3] == 'xref'
207
- overview_format << ident
208
- else
209
- overview_format << 'xref'
210
- end
211
- end
212
- overview_format
213
- end
214
-
215
- # TODO: complete implementation
216
- def stat
217
- end
218
-
219
- # Issues 'HEAD' command to NNTP server, returning raw response
220
- #
221
- # Throws CommandFailedError
222
- def head(args=nil)
223
- suffix = numbers_or_id(args)
224
- cmd = 'head'
225
- cmd = ['head', suffix].join " " if suffix
226
- send_cmd(cmd)
227
- response = read_response()
228
- case response[0][0..2]
229
- when '221'
230
- return response
231
- else
232
- raise CommandFailedError, response
233
- end
234
- end
235
-
236
- # Issues 'BODY' command to NNTP server, returning raw response.
237
- # options:: messageid|id
238
- #
239
- # Throws CommandFailedError
240
- def body(args=nil)
241
- suffix = args
242
- cmd = 'body'
243
- cmd = ['body', suffix].join " " if suffix
244
- send_cmd(cmd)
245
- response = read_response()
246
- case response[0][0..2]
247
- when '222'
248
- return response
249
- else
250
- raise CommandFailedError, response
251
- end
252
- end
253
-
254
- # Issues 'ARTICLE' command to NNTP server, returning raw response.
255
- # options:: messageid|id
256
- #
257
- # Throws CommandFailedError
258
- def article(args=nil)
259
- suffix = args
260
- cmd = 'article'
261
- cmd = ['article', suffix].join " " if suffix
262
- send_cmd(cmd)
263
- response = read_response()
264
- case response[0][0..2]
265
- when '220'
266
- return response
267
- else
268
- raise CommandFailedError, response
269
- end
270
- end
271
-
272
- def last_or_next(cmd)
273
- raise ProtocolError, "No group selected" unless @group
274
- send_cmd(cmd)
275
- response = read_response()[0]
276
- code = response[0..2]
277
- article = @group.articles.create
278
- case code
279
- when '223'
280
- code, id, msgid, what = response.split
281
- article.id = id
282
- article.messageid = msgid
283
- else
284
- raise CommandFailedError, response
285
- end
286
- response
287
- end
288
-
289
- # Issues the LAST command to the NNTP server, returning the raw response
290
- def last
291
- last_or_next("last")
292
- end
293
-
294
- def next
295
- last_or_next("next")
296
- end
297
-
298
- def create_grouplist(response)
299
- @grouplist = {}
300
- response.each_with_index do |line, idx|
301
- next if idx == 0
302
- break if line =~ /^\.\r\n$/
303
- groupinfo = line.split
304
- group = Group.new groupinfo.shift
305
- group.article_info = groupinfo
306
- @grouplist[group.name] = group
307
- end
308
- @grouplist
309
- end
310
-
311
- def send_cmd(cmd)
312
- debug "Sending: '#{cmd}'"
313
- @socket.write(cmd+"\r\n")
314
- end
315
-
316
- def read_response(force_oneline=false)
317
- response = ''
318
- str = ''
319
- ra = []
320
- linecnt = 0
321
- loop do
322
- str = @socket.readline
323
- ra << str
324
- break if force_oneline || (str == ".\r\n" || !str || (linecnt == 0 && ONELINE_STATUSES.include?(str[0..2])) )
325
- linecnt += 1
326
- end
327
- debug "Response: '#{ra}'"
328
- ra
329
- end
330
-
331
- def numbers_or_id(hash)
332
- return nil unless hash
333
- suffix = ''
334
- from = hash[:from]
335
- to = hash[:to]
336
- msgid = hash[:message_id]
337
- if from
338
- suffix = "#{from}-"
339
- suffix += "#{to}" if to
340
- elsif msgid
341
- suffix = "#{msgid}"
342
- end
343
- suffix
344
- end
345
-
346
- private :read_response, :numbers_or_id, :send_cmd, :last_or_next, :create_grouplist
347
-
348
- end
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:noet
360
+ # vim:sts=2:ts=2:sw=2:sta:et