ruby-net-nntp 0.2.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -1,407 +1,341 @@
1
- # = Net::NNTP class
1
+ # = Net::NNTP library
2
2
  # Author:: Anton Bangratz
3
- require 'socket'
4
- require 'thread'
5
- require 'timeout' # :nodoc:
6
- require 'net/nntp/group'
7
- require 'net/nntp/article'
8
- require 'net/nntp/version.rb'
3
+ #
9
4
  require 'rubygems'
10
- require 'log4r'
11
-
12
-
13
-
14
- module Net
5
+ require 'timeout'
6
+ require 'tmail'
7
+ require 'log4r'
8
+ require 'date'
9
+ require 'ostruct'
10
+ require 'net/nntp/version'
11
+ require 'net/nntp/request.rb'
12
+ require 'net/nntp/response.rb'
13
+
14
+ # Namespace
15
+ module Net
16
+ # Base class for connecting to an NNTP server and handling requests and responses.
15
17
  class NNTP
16
- include Timeout # :nodoc:
17
-
18
- # Statuses of one-line responses
19
- 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
20
-
21
- # Statuses of multiline responses
22
- MULTILINE_STATUSES = %w( 100 101 215 220 221 222 224 225 230 231 ).freeze
23
-
24
- # Error to indicate that NNTP Command failed gracefully
25
- class CommandFailedError < StandardError
26
- end
27
- # Error to indicate that a Protocol Error occured during NNTP command execution
28
- class ProtocolError < StandardError
29
- end
30
18
 
19
+ attr_accessor :host, :port, :timeout
20
+ attr_reader :last_response, :current_group, :current_article
31
21
 
32
- attr_reader :socket, :grouplist, :overview_format
33
- attr_accessor :response_timeout
34
22
 
23
+ # Setter for a Log4r style logger.
35
24
  def self.logger=(logger)
36
25
  @@logger = logger
37
26
  end
38
27
 
28
+ # Accessor for the Log4r style logger.
39
29
  def self.logger
40
30
  @@logger
41
31
  end
42
32
 
43
- def logger
44
- @@logger
45
- end
33
+ # Shortcut for Net::NNTP.logger.debug(message)
46
34
  def debug(message)
47
- @@logger.debug(message)
48
- end
49
-
50
- # initializes NNTP class with host, port and timeout values
51
- def initialize(host, port = 119, response_timeout = 10)
52
- @host = host
53
- @port = port
54
- @response_timeout = response_timeout
55
- @socket_class = TCPSocket
56
- @group = nil
57
- @@logger ||= Log4r::Logger['net::nntp'] || Log4r::Logger.new('net::nntp')
35
+ Net::NNTP.logger.debug(message)
58
36
  end
59
37
 
60
- # Actually connects to NNTP host and port given in new()
61
- def connect
62
- @socket = @socket_class.new(@host, @port)
63
- @welcome = read_response()
64
- debug "Welcome: #{@welcome[0]} "
65
- @welcome
66
- end
67
-
68
- # Checks if socket is still valid and connected
69
- def connected?
70
- @socket && !@socket.closed?
38
+ # Initializes an NNTP instance.
39
+ #
40
+ # Takes an option hash as argument. Keys being processed are :host, :port and :timeout
41
+ def initialize(args={})
42
+ @host = args[:host] || default_host()
43
+ @port = args[:port] || default_port()
44
+ @timeout = args[:timeout] || default_timeout()
45
+ @current_request = nil
46
+ if block_given?
47
+ begin
48
+ yield self
49
+ ensure
50
+ close
51
+ end
52
+ end
71
53
  end
72
54
 
73
- # Sends QUIT command to server and closes the connection (unless force_close is set to false). Returns response.
74
- def quit(force_close=true)
75
- send_cmd "QUIT"
76
- response = read_response
77
- close if force_close
78
- response
55
+ # Sets up the connection to a TCPSocket, using #host and #port accessors.
56
+ #
57
+ # To use different server and port values, use the accessors.
58
+ #
59
+ # Reads and returns the response (should be a PostingOK or PostingProhibited response, or a generic response).
60
+ #
61
+ # == Usage Example
62
+ #
63
+ # nntp = Net::NNTP.new
64
+ # nntp.server = 'my.server.example.com'
65
+ # nntp.port = 22119
66
+ # if nntp.connect
67
+ # # ... do stuff ...
68
+ # end
69
+ def connect
70
+ @socket = TCPSocket.new(host(), port());
71
+ @connected = true
72
+ @current_request = nil
73
+ @last_response = read_response()
74
+ end
75
+
76
+ def reconnect
77
+ close unless @socket.closed?
78
+ connect
79
+ authenticate *@credentials if @credentials
80
+ process @current_group.request if @current_group
81
+ process @current_article.request if @current_article
79
82
  end
80
83
 
81
-
82
- # Closes connection. If not reconnected, subsequent calls of commands raise exceptions
83
84
  def close
84
- debug 'closing connection per request'
85
- @socket.close unless socket.closed?
85
+ @socket.close unless @socket.closed?
86
86
  end
87
87
 
88
- def has_xover?
89
- !help.select {|i| i =~ /\bxover\b/i}.empty?
88
+ # Returns true if #connect was successful and the socket is connected.
89
+ def connected?
90
+ @socket && !@socket.closed? && @connected
91
+ end
92
+
93
+ def read_response
94
+ begin
95
+ debug "reading from socket: #{@socket}"
96
+ line=nil
97
+ Timeout::timeout(@timeout) {
98
+ line = @socket.readline
99
+ }
100
+ debug("line from socket: #{line}")
101
+ response = Response.create(@current_request, line)
102
+ response
103
+ rescue EOFError
104
+ ensure
105
+ @current_request = nil
106
+ end
90
107
  end
108
+ private :read_response
91
109
 
92
- def has_over?
93
- !help.select {|i| i =~ /\bover\b/i}.empty?
94
- end
95
- # Uses authinfo commands to authenticate. Timeout for first command is set to 10 seconds.
110
+ # Processes a request.
96
111
  #
97
- # Returns true on success, false on failure.
98
- def authenticate(user, pass)
99
- cmd = "authinfo user #{user}"
100
- debug "Authenticating: Sending #{cmd}"
101
- send_cmd cmd
102
- response_array = read_response()
103
- response = response_array[0]
104
- debug "Authenticating: Response #{response}"
105
- if response[0..2] == '381' then
106
- cmd = "authinfo pass #{pass}"
107
- debug "Authenticating: Sending #{cmd}"
108
- send_cmd cmd
109
- response_array = read_response()
110
- response = response_array[0]
111
- debug "Authenticating: Response #{response}"
112
+ # +request+ must be a Net::NNTP::Request subclass. Certain requests (Authinfo and requests that require a two-stage
113
+ # response/request cycle will try to process these two stages gracefully. For multiline responses, the body will be
114
+ # processed, too.
115
+ #
116
+ # Net::NNTP::GroupSelected responses will be recorded in *current_group*, Net::NNTP::ArticleSelected responses in
117
+ # *current_article*.
118
+ #
119
+ # Returns the last response.
120
+ def process(request)
121
+ if connected?
122
+ @current_request = request
123
+ debug("Sending #{request.command}")
124
+ @socket.write(request.command)
125
+ @last_response = read_response()
126
+ if @last_response.needs_article?
127
+ debug "Sending article: #{request.dotstuff}"
128
+ request.dotstuff.split(/\r?\n/).each do |line|
129
+ line << "\r\n"
130
+ @socket.write line
131
+ end
132
+ @socket.write ".\r\n"
133
+ @last_response = read_response()
134
+ end
135
+ if (@last_response.multiline?)
136
+ debug "Response is multiline"
137
+ @last_response.body = @socket
138
+ end
139
+ @socket.close if @last_response.force_close?
140
+ @last_response
141
+ if (GroupSelected === @last_response)
142
+ @current_group = @last_response
143
+ elsif (ArticleSelected === @last_response)
144
+ @current_article = @last_response
145
+ else
146
+ @last_response
147
+ end
112
148
  end
113
- return response && response[0..2] == '281'
114
149
  end
115
150
 
116
- # Issues 'GROUP' command to NNTP Server and creates new active group from returning data.
151
+ # Proxy for authentication request/response cycles.
117
152
  #
118
- # Throws CommandFailedError
153
+ # See Net::NNTP::Authinfo for parameters.
119
154
  #
120
- def group(group)
121
- send_cmd "group #{group}"
122
- response = read_response(true)
123
- responsecode, cnt, first, last, name = response[0].split
124
- if responsecode == '211'
125
- @group = Group.new(name)
126
- @group.article_info = [cnt, first, last]
127
- @group
155
+ # Returns +true+ if the authentication has been accepted.
156
+ def authenticate(user, pass)
157
+ @credentials = [user, pass]
158
+ auth_request = Authinfo.new('user', user)
159
+ if PasswordRequired === process(auth_request)
160
+ auth_request = Authinfo.new('pass', pass)
161
+ AuthenticationAccepted === process(auth_request)
128
162
  else
129
- raise CommandFailedError, response[0]
163
+ false
130
164
  end
131
165
  end
132
166
 
133
- # Issues 'HELP' command to NNTP Server, and returns raw response.
134
- def help
135
- send_cmd("help")
136
- read_response()
167
+ # Proxy for Net::NNTP::Article requests and processing.
168
+ #
169
+ # See Net::NNTP::Article for parameters
170
+ #
171
+ # Returns the response.
172
+ def article(opts=nil)
173
+ request = Article.new(opts)
174
+ process(request)
137
175
  end
138
176
 
139
- # Issues 'XHDR' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
140
- # otherwise.
177
+ # Proxy for Net::NNTP::Body requests and processing.
141
178
  #
142
- # TODO:: Implement XHDR header <message-id>
143
- def xhdr(header, groupname=nil, rest=nil)
144
- raise CommandFailedError, 'No group selected' unless @group || groupname
145
- if @group.nil?
146
- group(groupname)
147
- elsif groupname.nil?
148
- else
149
- @group.name != groupname
150
- group(groupname)
151
- end
152
- cmd = "xhdr #{header}"
153
- suffix = numbers_or_id(rest)
154
- send_cmd([cmd, suffix].join(' '))
155
- read_response()
179
+ # See Net::NNTP::Body for parameters
180
+ #
181
+ # Returns the response.
182
+ def body(opts)
183
+ request = Body.new(opts)
184
+ process(request)
156
185
  end
157
186
 
158
- # Issues 'XOVER' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
159
- # otherwise. If no group is selected nor given, raises error.
160
- # Parameter 'rest' can be in the form of :from => number, :to => number or :messageid => 'messageid',
161
- # if not set, a 'next' command is issued to the server prior to the xover command
187
+ # Proxy for Net::NNTP::Head requests and processing.
162
188
  #
163
- def xover(groupname=nil, rest=nil)
164
- raise CommandFailedError, 'No group selected' unless @group || groupname
165
- if @group.nil?
166
- group(groupname)
167
- elsif groupname.nil?
168
- else
169
- @group.name != groupname
170
- group(groupname)
171
- end
172
- debug "Selected Group: #{@group.name}"
173
- self.next unless rest
174
- prefix = "xover"
175
- suffix = numbers_or_id(rest)
176
- cmd = [prefix, suffix].join ' '
177
- send_cmd(cmd)
178
- response = nil
179
- timeout(@response_timeout) do
180
- response = read_response()
181
- end
182
- response
189
+ # See Net::NNTP::Head for parameters
190
+ #
191
+ # Returns the response.
192
+ def head(opts)
193
+ request = Head.new(opts)
194
+ process(request)
183
195
  end
184
196
 
185
- def newnews(groups='*', date=nil, time=nil, gmt=true)
186
- gmt_str = gmt ? '"GMT"' : ""
187
- debug groups
188
- debug date
189
- debug time
190
- debug gmt_str
191
- cmd = 'newnews %s %s %s %s' % [groups, date, time, gmt_str]
192
- debug "Sending #{cmd}"
193
- send_cmd(cmd)
194
- response = nil
195
- response = read_response
196
- response
197
+ # Proxy for Net::NNTP::Help requests and processing.
198
+ #
199
+ # Returns the response.
200
+ def help
201
+ process Help.new
197
202
  end
198
203
 
199
- # Issues 'LISTGROUP' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
200
- # otherwise.
204
+ # Proxy for Net::NNTP::Last requests and processing.
201
205
  #
202
- def listgroup(groupname=nil)
203
- raise CommandFailedError, 'No group selected' unless @group || groupname
204
- if @group.nil?
205
- group(groupname)
206
- elsif @group.name != groupname
207
- group(groupname)
208
- end
209
- debug "Selected Group: #{@group.name}"
210
- send_cmd('listgroup')
211
- read_response()
206
+ # Returns the response.
207
+ def last
208
+ process Last.new
212
209
  end
213
210
 
214
- # Issues 'LIST' command to NNTP Server. Depending on server capabilities and given keyword, can either set overview
215
- # format (if called with 'overview.fmt') or create a grouplist (see Attributes)
216
- #
217
- # Throws CommandFailedError
218
-
219
- def list(keyword=nil, pattern=nil)
220
- cmd = ['list', keyword, pattern].join ' '
221
- send_cmd(cmd)
222
- list = read_response()
223
- responsecode = list[0][0..2]
224
- case responsecode
225
- when '215'
226
- case keyword
227
- when /overview.fmt/
228
- @overview_format_raw = list
229
- @overview_format = Net::NNTP.parse_overview_format list
230
- else
231
- create_grouplist(list)
232
- list
233
- end
234
-
235
- when '501', '503', '500'
236
- raise CommandFailedError, list
237
- end
211
+ # Proxy for Net::NNTP::Next requests and processing.
212
+ #
213
+ def next
214
+ process Next.new
238
215
  end
239
216
 
240
- # prepares overview format as read from server, used by Net::NNTP::Article and list()
241
- def self.parse_overview_format(format)
242
- overview_format = %w{number}
243
- lines = format
244
- lines.each do |line|
245
- next if line[0] == ?2 || line[0] == ?. || line == 'full'
246
- ident = line.scan(/\w/).join.downcase
247
- unless ident[0..3] == 'xref'
248
- overview_format << ident
249
- else
250
- overview_format << 'xref'
251
- end
252
- end
253
- overview_format
217
+ # Proxy for Net::NNTP::Group requests and processing.
218
+ #
219
+ # See Net::NNTP::Group for parameters
220
+ #
221
+ # Returns the response.
222
+ def group(name)
223
+ process Group.new(name)
254
224
  end
255
225
 
256
- # TODO: complete implementation
257
- def stat
226
+ # Proxy for Net::NNTP::List requests and processing.
227
+ #
228
+ # See Net::NNTP::List for parameters
229
+ #
230
+ # Returns the response.
231
+ def list(keyword=nil, rest={})
232
+ process List.new(keyword, rest)
258
233
  end
259
234
 
260
- # Issues 'HEAD' command to NNTP server, returning raw response
261
- #
262
- # Throws CommandFailedError
263
- def head(args=nil)
264
- suffix = numbers_or_id(args)
265
- cmd = 'head'
266
- cmd = ['head', suffix].join " " if suffix
267
- send_cmd(cmd)
268
- response = read_response()
269
- case response[0][0..2]
270
- when '221'
271
- return response
272
- else
273
- raise CommandFailedError, response
274
- end
235
+ # Proxy for Net::NNTP::Listgroup request and processing
236
+ #
237
+ # See Net::NNTP::Listgroup for parameters
238
+ #
239
+ # Returns the response.
240
+ def listgroup(groupname=nil, range={})
241
+ process Listgroup.new(groupname, range)
275
242
  end
276
243
 
277
- # Issues 'BODY' command to NNTP server, returning raw response as array of lines.
278
- # options:: messageid|id
279
- #
280
- # Throws CommandFailedError
281
- def body(args=nil)
282
- suffix = args
283
- cmd = 'body'
284
- cmd = ['body', suffix].join " " if suffix
285
- send_cmd(cmd)
286
- response = read_response()
287
- case response[0][0..2]
288
- when '222'
289
- return response
290
- else
291
- raise CommandFailedError, response
292
- end
244
+ # Proxy for Net::NNTP::Quit requests and processing.
245
+ #
246
+ def quit
247
+ process Quit.new
293
248
  end
294
249
 
295
- # Issues 'ARTICLE' command to NNTP server, returning raw response.
296
- # options:: messageid|id
297
- #
298
- # Throws CommandFailedError
299
- def article(args=nil)
300
- suffix = args
301
- cmd = 'article'
302
- cmd = ['article', suffix].join " " if suffix
303
- send_cmd(cmd)
304
- response = read_response()
305
- case response[0][0..2]
306
- when '220'
307
- return response
308
- else
309
- raise CommandFailedError, response
310
- end
250
+ # Proxy for Net::NNTP::Stat requests and processing.
251
+ #
252
+ # See Net::NNTP::Stat for parameters
253
+ #
254
+ # Returns the response.
255
+ def stat(opts=nil)
256
+ request = Stat.new(opts)
257
+ process(request)
311
258
  end
312
259
 
313
- def last_or_next(cmd)
314
- raise ProtocolError, "No group selected" unless @group
315
- send_cmd(cmd)
316
- response = read_response()[0]
317
- code = response[0..2]
318
- article = @group.articles.create
319
- case code
320
- when '223'
321
- code, id, msgid, what = response.split
322
- article.number = id
323
- article.messageid = msgid
324
- else
325
- raise CommandFailedError, response
326
- end
327
- response
260
+ # Proxy for Net::NNTP::Hdr requests and processing.
261
+ #
262
+ # See Net::NNTP::Hdr for parameters
263
+ #
264
+ # Returns the response.
265
+ def hdr(field, opts)
266
+ request = Hdr.new(field, opts)
267
+ process(request)
328
268
  end
329
269
 
330
- # Issues the LAST command to the NNTP server, returning the raw response
331
- def last
332
- last_or_next("last")
270
+ # Proxy for Net::NNTP::Xhdr requests and processing.
271
+ #
272
+ # See Net::NNTP::Xhdr for parameters
273
+ #
274
+ # Returns the response.
275
+ def xhdr(field, opts)
276
+ request = Xhdr.new(field, opts)
277
+ process(request)
333
278
  end
334
279
 
335
- # Issues the NEXT command to the NNTP server, returning the raw response
336
- def next
337
- last_or_next("next")
280
+
281
+ # Proxy for Net::NNTP::Over requests and processing.
282
+ #
283
+ # See Net::NNTP::Over for parameters
284
+ #
285
+ # Returns the response.
286
+ def over(range=nil)
287
+ request = Over.new(range)
288
+ process(request)
338
289
  end
339
290
 
340
- def create_grouplist(response)
341
- @grouplist = {}
342
- response.each_with_index do |line, idx|
343
- next if idx == 0
344
- break if line =~ /^\.$/
345
- groupinfo = line.split
346
- group = Group.new groupinfo.shift
347
- group.article_info = groupinfo
348
- @grouplist[group.name] = group
349
- end
350
- @grouplist
291
+ # Proxy for Net::NNTP::Xover requests and processing.
292
+ #
293
+ # See Net::NNTP::Xover for parameters
294
+ #
295
+ # Returns the response.
296
+ def xover(range=nil)
297
+ request = Xover.new(range)
298
+ process(request)
351
299
  end
352
300
 
353
- def send_cmd(cmd)
354
- debug "Sending: '#{cmd}'"
355
- @socket.write(cmd+"\r\n")
301
+
302
+ # Proxy for Net::NNTP::Post requests and processing.
303
+ #
304
+ # See Net::NNTP::Post for parameters
305
+ #
306
+ # Returns the response.
307
+ def post(body)
308
+ request = Post.new
309
+ request.body = body
310
+ process(request)
356
311
  end
357
312
 
358
- # Reads response from server socket. On multiline responses according to RFC, the response will be returned as an
359
- # array of lines, else as an array including the response on one line. Parameter force_online can be used to
360
- # override this behaviour and just read one line from the server.
361
- def read_response(force_oneline=false)
362
- response = ''
363
- str = ''
364
- ra = []
365
- linecnt = 0
366
- loop do
367
- str = @socket.readline
368
- #debug " -> got line: '#{str}'"
369
- str = str[1..-1] if str && str[0..1] == '..'
370
- ra << str.chomp if str
371
- break if force_oneline || (!str || str.chomp == "." || (linecnt == 0 && ONELINE_STATUSES.include?(str[0..2])) )
372
- linecnt += 1
373
- end
374
- debug "Response: '#{ra.inspect}'"
375
- ra
313
+
314
+ # Proxy for Net::NNTP::Ihave requests and processing.
315
+ #
316
+ # See Net::NNTP::Ihave for parameters
317
+ #
318
+ # Returns the response.
319
+ def ihave(id, body)
320
+ request = Ihave.new id
321
+ request.body = body
322
+ process(request)
323
+ end
324
+ private
325
+ def default_host
326
+ 'localhost'
376
327
  end
377
328
 
378
- # Parses a hash in the format {:from => int, :to => int} or {:message_id => int}
379
- # and returns a string. :to is optional, :from has precedence over :message_id if
380
- # both are given
381
- #
382
- # Examples:
383
- # numbers_or_id(:from => 1, :to => 100) #=> returns '1-100'
384
- # numbers_or_id(:from => 1) #=> returns '1-'
385
- # numbers_or_id(:message_id = '<abc>' #=> returns '<abc>'
386
- # numbers_or_id(:from => 5, :message_id = '<abc>' #=> returns 'f-'
387
- def numbers_or_id(hash)
388
- return nil unless hash
389
- suffix = ''
390
- from = hash[:from]
391
- to = hash[:to]
392
- msgid = hash[:message_id]
393
- if from
394
- suffix = "#{from}-"
395
- suffix += "#{to}" if to
396
- elsif msgid
397
- suffix = "#{msgid}"
398
- end
399
- suffix
329
+ def default_port
330
+ 119
400
331
  end
401
332
 
402
- private :read_response, :numbers_or_id, :send_cmd, :last_or_next, :create_grouplist
333
+ def default_timeout
334
+ 15
335
+ end
403
336
 
404
337
  end
405
338
  end
406
339
 
340
+
407
341
  # vim:sts=2:ts=2:sw=2:sta:et