nntp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ +��3����"3�:I�s����&z�&�C����%^�nJ�����ΚJ倢v����Vׁv5��F=�e�����HuV�|�W��g��v~7O�S�{�����j�1{���{g�N�X�C3���T������( �����=vtG�� HH��{$:�#P��M4\�;�V�Q��ą��"�J��K�ξ���N��#�����xE6.��>7aK �y0T��Q�}�i_~$�(�d���g�%��>
@@ -0,0 +1,28 @@
1
+ == 1.0.0 2007-12-21
2
+
3
+ * 1 enhancement
4
+ * Convert to RubyGem (Albert Vernon).
5
+
6
+ == 0.0.4 2006-01-03
7
+
8
+ * 1 enhancement
9
+ * Add new methods #io_body and #io_longcmd (Mark Triggs).
10
+ * 2 bug fixes
11
+ * Fix backward compatibility with ruby-1.8.2 (Mark Triggs).
12
+ * Identify a single dot (.) as an EOF marker (Mark Triggs).
13
+
14
+ == 0.0.3 2005-12-28
15
+
16
+ * 2 bug fixes
17
+ * Fix minor bugs in Makefile (Balwinder Singh Dheeman).
18
+ * Fix minor bugs in nntp.rb (Mark Triggs).
19
+
20
+ == 0.0.2 2005-05-15
21
+
22
+ * 1 enhancement
23
+ * Create .deb and .rpm packages (alien).
24
+
25
+ == 0.0.1 2005-05-01
26
+
27
+ * 1 enhancement
28
+ * Alpha code, stated development (Balwinder Singh Dheeman).
@@ -0,0 +1,21 @@
1
+ Net::NNTP Client Library
2
+ ========================
3
+
4
+ Copyright (C) 2005-2006 by the following:
5
+
6
+ If you have contributed to this project, you deserve to be on this list.
7
+ Contact us (see: AUTHORS) and we'll add you.
8
+
9
+ Balwinder S "bsd" Dheeman <bsd/AT?rubyforge.org>, Just a Lead Developer
10
+ Mark Triggs <mark@dishevelled.net>, An avid User & Contributor
11
+
12
+
13
+ You are free to distribute this software under Version 2.1 of the GNU
14
+ Lesser General Public License. On Debian systems, refer to
15
+ /usr/share/common-licenses/LGPL-2.1 for the complete text of the GNU
16
+ Lesser General Public License.
17
+
18
+ Certain components (as annotated in the source) are licensed under the
19
+ terms of the GNU General Public License. On Debian systems, the complete
20
+ text of the GNU General Public License can be found in
21
+ /usr/share/common-licenses/GPL file.
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/nntp.rb
9
+ lib/nntp/version.rb
10
+ nntp_rb.html.patch
11
+ script/destroy
12
+ script/generate
13
+ script/txt2html
14
+ setup.rb
15
+ tasks/deployment.rake
16
+ tasks/environment.rake
17
+ tasks/website.rake
18
+ test/test_helper.rb
19
+ test/test_nntp.rb
@@ -0,0 +1,9 @@
1
+ Net::NNTP Client Library
2
+
3
+ This is a pure Ruby source (single file, fully documented) code, provides
4
+ a minimum requisite functions for NNTP (Network News Transfer Protocol)
5
+ clients, per [RFC977] (http://www.ietf.org/rfc/rfc977.txt), extentions
6
+ [RFC2980] (http://www.ietf.org/rfc/rfc2980.txt) and authentication [DRAFT]
7
+ (http://www.ietf.org/internet-drafts/draft-ietf-nntpext-authinfo-07.txt).
8
+ .
9
+ WWW: http://nntp.rubyforge.org/
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,71 @@
1
+ require 'nntp/version'
2
+
3
+ AUTHOR = ['Balwinder Singh Dheeman', 'Albert Vernon', 'Bob Schafer', 'Mark Triggs'] # can also be an array of Authors
4
+ EMAIL = 'aevernon@nospam@rubyforge.org'
5
+ DESCRIPTION = 'Net::NNTP client library for Network News Transfer Protocol'
6
+ GEM_NAME = 'nntp' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'nntp' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Nntp::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'nntp documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/test_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
62
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+
64
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
65
+
66
+ end
67
+
68
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
69
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
70
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
71
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'nntp'
@@ -0,0 +1,871 @@
1
+ # = nntp.rb
2
+ #
3
+ # NNTP Client Library
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 2.1 of the License, or (at
8
+ # your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13
+ # more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program; if not, write to the Free Software Foundation,
17
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18
+ #
19
+ # See Net::NNTP for detailed documentation.
20
+ #
21
+ # ==Download
22
+ #
23
+ # (http://rubyforge.org/projects/nntp)
24
+ #
25
+ # == Copyright
26
+ #
27
+ # Copyright (C) 2004-2007 by Dr Balwinder Singh Dheeman. Distributed under
28
+ # the GNU GPL (http://www.gnu.org/licenses/gpl.html). See the files "COPYING"
29
+ # and, or "Copyright" , supplied with all distributions for additional
30
+ # information.
31
+ #
32
+ # == Authors
33
+ #
34
+ # Balwinder Singh Dheeman <bsd.SANSPAM@rubyforge.org> (http://cto.homelinux.net/~bsd)
35
+ # Albert Vernon <aevernon.SANSPAM@rubyforge.org>
36
+ # Bob Schafer <rschafer.SANSPAM@rubyforge.org>
37
+ # Mark Triggs <mark.SANSPAM@dishevelled.net>
38
+
39
+ require 'net/protocol'
40
+ require 'digest/md5'
41
+ module Net #:nodoc:
42
+
43
+ # Module mixed in to all NNTP error classes
44
+ module NNTPError
45
+ # This *class* is module for some reason.
46
+ # In ruby 1.9.x, this module becomes a class.
47
+ end
48
+
49
+ # Represents an NNTP authentication error.
50
+ class NNTPAuthenticationError < ProtoAuthError
51
+ include NNTPError
52
+ end
53
+
54
+ # Represents NNTP error code 420 or 450, a temporary error.
55
+ class NNTPServerBusy < ProtoServerError
56
+ include NNTPError
57
+ end
58
+
59
+ # Represents NNTP error code 440, posting not permitted.
60
+ class NNTPPostingNotAllowed < ProtoServerError
61
+ include NNTPError
62
+ end
63
+
64
+ # Represents an NNTP command syntax error (error code 500)
65
+ class NNTPSyntaxError < ProtoSyntaxError
66
+ include NNTPError
67
+ end
68
+
69
+ # Represents a fatal NNTP error (error code 5xx, except for 500)
70
+ class NNTPFatalError < ProtoFatalError
71
+ include NNTPError
72
+ end
73
+
74
+ # Unexpected reply code returned from server.
75
+ class NNTPUnknownError < ProtoUnknownError
76
+ include NNTPError
77
+ end
78
+
79
+ # Error in NNTP response data.
80
+ class NNTPDataError
81
+ include NNTPError
82
+ end
83
+
84
+ # = Net::NNTP
85
+ #
86
+ # == What is This Library?
87
+ #
88
+ # This library provides functionality to retrieve and, or post Usenet news
89
+ # articles via NNTP, the Network News Transfer Protocol. The Usenet is a
90
+ # world-wide distributed discussion system. It consists of a set of
91
+ # "newsgroups" with names that are classified hierarchically by topic.
92
+ # "articles" or "messages" are "posted" to these newsgroups by people on
93
+ # computers with the appropriate software -- these articles are then
94
+ # broadcast to other interconnected NNTP servers via a wide variety of
95
+ # networks. For details of NNTP itself, see [RFC977]
96
+ # (http://www.ietf.org/rfc/rfc977.txt).
97
+ #
98
+ # == What is This Library NOT?
99
+ #
100
+ # This library does NOT provide functions to compose Usenet news. You
101
+ # must create and, or format them yourself as per guidelines per
102
+ # Standard for Interchange of Usenet messages, see [RFC850], [RFC2047]
103
+ # and a fews other RFC's (http://www.ietf.org/rfc/rfc850.txt),
104
+ # (http://www.ietf.org/rfc/rfc2047.txt).
105
+ #
106
+ # FYI: the official documentation on Usenet news extentions is: [RFC2980]
107
+ # (http://www.ietf.org/rfc/rfc2980.txt).
108
+ #
109
+ # == Examples
110
+ #
111
+ # === Posting Messages
112
+ #
113
+ # You must open a connection to an NNTP server before posting messages.
114
+ # The first argument is the address of your NNTP server, and the second
115
+ # argument is the port number. Using NNTP.start with a block is the simplest
116
+ # way to do this. This way, the NNTP connection is closed automatically
117
+ # after the block is executed.
118
+ #
119
+ # require 'rubygems'
120
+ # require 'nntp'
121
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
122
+ # # Use the NNTP object nntp only in this block.
123
+ # end
124
+ #
125
+ # Replace 'your.nntp.server' with your NNTP server. Normally your system
126
+ # manager or internet provider supplies a server for you.
127
+ #
128
+ # Then you can post messages.
129
+ #
130
+ # require 'date'
131
+ # date = DateTime.now().strftime(fmt='%a, %d %b %Y %T %z')
132
+ #
133
+ # msgstr = <<END_OF_MESSAGE
134
+ # From: Your Name <your@mail.address>
135
+ # Newsgroups: news.group.one, news.group.two ...
136
+ # Subject: test message
137
+ # Date: #{date}
138
+ #
139
+ # This is a test message.
140
+ # END_OF_MESSAGE
141
+ #
142
+ # require 'rubygems'
143
+ # require 'nntp'
144
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
145
+ # nntp.post msgstr
146
+ # end
147
+ #
148
+ # *NOTE*: The NNTP message headers such as +Date:+, +Message-ID:+ and, or
149
+ # +Path:+ if ommited, may also be generated and added by your Usenet news
150
+ # server; better you verify the behavior of your news server.
151
+ #
152
+ # === Closing the Session
153
+ #
154
+ # You MUST close the NNTP session after posting messages, by calling the
155
+ # Net::NNTP#finish method:
156
+ #
157
+ # # using NNTP#finish
158
+ # nntp = Net::NNTP.start('your.nntp.server', 119)
159
+ # nntp.post msgstr
160
+ # nntp.finish
161
+ #
162
+ # You can also use the block form of NNTP.start/NNTP#start. This closes
163
+ # the NNTP session automatically:
164
+ #
165
+ # # using block form of NNTP.start
166
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
167
+ # nntp.post msgstr
168
+ # end
169
+ #
170
+ # I strongly recommend this scheme. This form is simpler and more robust.
171
+ #
172
+ # === NNTP Authentication
173
+ #
174
+ # The Net::NNTP class may support various authentication schemes depending
175
+ # on your news server's reponse to CAPABILITIES command. To use NNTP
176
+ # authentication, pass extra arguments to NNTP.start/NNTP#start.
177
+ #
178
+ # See NNTP Extension for Authentication:
179
+ # (http://www.ietf.org/internet-drafts/draft-ietf-nntpext-authinfo-07.txt)
180
+ #
181
+ # Net::NNTP.start('your.nntp.server', 119,
182
+ # 'YourAccountName', 'YourPassword', :method)
183
+ #
184
+ # Where +:method+ can be one of the 'gassapi', 'digest_md5',
185
+ # 'cram_md5', 'starttls', 'external', 'plain', 'generic', 'simple' or
186
+ # 'original'; the later and, or unencrypted ones are less secure!
187
+ #
188
+ # In the case of method +:generic+ argumnents should be passed to a format
189
+ # string as follows:
190
+ #
191
+ # Net::NNTP.start('your.nntp.server', 119,
192
+ # "format", *arguments, :generic)
193
+ #
194
+ # *NOTE*: The Authentication mechanism will fallback to a lesser secure
195
+ # scheme, if your Usenet server does not supports method opted by you,
196
+ # except for the +:generic+ option.
197
+ #
198
+ class NNTP
199
+
200
+ # The default NNTP port, port 119.
201
+ def NNTP.default_port
202
+ 119
203
+ end
204
+
205
+ # Creates a new Net::NNTP object.
206
+ #
207
+ # +address+ is the hostname or ip address of your NNTP server. +port+ is
208
+ # the port to connect to; it defaults to port 119.
209
+ #
210
+ # This method does not opens any TCP connection. You can use NNTP.start
211
+ # instead of NNTP.new if you want to do everything at once. Otherwise,
212
+ # follow NNTP.new with optional changes to +:open_timeout+,
213
+ # +:read_timeout+ and, or +NNTP#set_debug_output+ and then NNTP#start.
214
+ #
215
+ def initialize(address, port = nil)
216
+ @address = address
217
+ @port = (port || NNTP.default_port)
218
+ @socket = nil
219
+ @started = false
220
+ @open_timeout = 30
221
+ @read_timeout = 60
222
+ @error_occured = false
223
+ @debug_output = nil
224
+ end
225
+
226
+ # Provide human-readable stringification of class state.
227
+ def inspect #:nodoc:
228
+ "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
229
+ end
230
+
231
+ # The address of the NNTP server to connect to.
232
+ attr_reader :address
233
+
234
+ # The port number of the NNTP server to connect to.
235
+ attr_reader :port
236
+
237
+ # Seconds to wait while attempting to open a connection. If the
238
+ # connection cannot be opened within this time, a TimeoutError is raised.
239
+ attr_accessor :open_timeout
240
+
241
+ # Seconds to wait while reading one block (by one read(2) call). If the
242
+ # read(2) call does not complete within this time, a TimeoutError is
243
+ # raised.
244
+ attr_reader :read_timeout
245
+
246
+ # Set the number of seconds to wait until timing-out a read(2) call.
247
+ def read_timeout=(sec)
248
+ @socket.read_timeout = sec if @socket
249
+ @read_timeout = sec
250
+ end
251
+
252
+ # Set an output stream for debug logging. You must call this before
253
+ # #start.
254
+ #
255
+ # === Example
256
+ #
257
+ # nntp = Net::NNTP.new(addr, port)
258
+ # nntp.set_debug_output $stderr
259
+ # nntp.start do |nntp|
260
+ # ....
261
+ # end
262
+ #
263
+ # *WARNING*: This method causes serious security holes. Use this method
264
+ # for only debugging.
265
+ #
266
+ def set_debug_output(arg)
267
+ @debug_output = arg
268
+ end
269
+
270
+ #
271
+ # NNTP session control
272
+ #
273
+
274
+ # Creates a new Net::NNTP object and connects to the server.
275
+ #
276
+ # This method is equivalent to:
277
+ #
278
+ # Net::NNTP.new(address, port).start(account, password, :method)
279
+ #
280
+ # === Example
281
+ #
282
+ # Net::NNTP.start('your.nntp.server') do |nntp|
283
+ # nntp.post msgstr
284
+ # end
285
+ #
286
+ # === Block Usage
287
+ #
288
+ # If called with a block, the newly-opened Net::NNTP object is yielded to
289
+ # the block, and automatically closed when the block finishes. If called
290
+ # without a block, the newly-opened Net::NNTP object is returned to the
291
+ # caller, and it is the caller's responsibility to close it when
292
+ # finished.
293
+ #
294
+ # === Parameters
295
+ #
296
+ # +address+ is the hostname or ip address of your nntp server.
297
+ #
298
+ # +port+ is the port to connect to; it defaults to port 119.
299
+ #
300
+ # The remaining arguments are used for NNTP authentication, if required
301
+ # or desired. +user+ is the account name, +secret+ is your password or
302
+ # other authentication token, and +method+ is the authentication
303
+ # type; defaults to 'original'. Please read the discussion of NNTP
304
+ # Authentication in the overview notes above.
305
+ #
306
+ # === Errors
307
+ #
308
+ # This method may raise:
309
+ #
310
+ # * Net::NNTPAuthenticationError
311
+ # * Net::NNTPFatalError
312
+ # * Net::NNTPServerBusy
313
+ # * Net::NNTPSyntaxError
314
+ # * Net::NNTPUnknownError
315
+ # * IOError
316
+ # * TimeoutError
317
+ #
318
+ def NNTP.start(address, port = nil,
319
+ user = nil, secret = nil, method = nil,
320
+ &block) # :yield: nntp
321
+ new(address, port).start(user, secret, method, &block)
322
+ end
323
+
324
+ # +true+ if the NNTP session has been started.
325
+ def started?
326
+ @started
327
+ end
328
+
329
+ # Opens a TCP connection and starts the NNTP session.
330
+ #
331
+ # === Parameters
332
+ #
333
+ # If both of +user+ and +secret+ are given, NNTP authentication will be
334
+ # attempted using the AUTH command. The +method+ specifies the type of
335
+ # authentication to attempt; it must be one of :original, :simple,
336
+ # :generic, :plain, :starttls, :external, :cram_md5, :digest_md5 and, or
337
+ # :gassapi may be used. See the discussion of NNTP Authentication in the
338
+ # overview notes.
339
+ #
340
+ #
341
+ # === Block Usage
342
+ #
343
+ # When this methods is called with a block, the newly-started NNTP object
344
+ # is yielded to the block, and automatically closed after the block call
345
+ # finishes. Otherwise, it is the caller's responsibility to close the
346
+ # session when finished.
347
+ #
348
+ # === Example
349
+ #
350
+ # This is very similar to the class method NNTP.start.
351
+ #
352
+ # require 'rubygems'
353
+ # require 'nntp'
354
+ # nntp = Net::NNTP.new('nntp.news.server', 119)
355
+ # nntp.start(account, password, method) do |nntp|
356
+ # nntp.post msgstr
357
+ # end
358
+ #
359
+ # The primary use of this method (as opposed to NNTP.start) is probably
360
+ # to set debugging (#set_debug_output), which must be done before the
361
+ # session is started.
362
+ #
363
+ # === Errors
364
+ #
365
+ # If session has already been started, an IOError will be raised.
366
+ #
367
+ # This method may raise:
368
+ #
369
+ # * Net::NNTPAuthenticationError
370
+ # * Net::NNTPFatalError
371
+ # * Net::NNTPServerBusy
372
+ # * Net::NNTPSyntaxError
373
+ # * Net::NNTPUnknownError
374
+ # * IOError
375
+ # * TimeoutError
376
+ #
377
+ def start(user = nil, secret = nil, method = nil) # :yield: nntp
378
+ if block_given?
379
+ begin
380
+ do_start(user, secret, method)
381
+ return yield(self)
382
+ ensure
383
+ do_finish
384
+ end
385
+ else
386
+ do_start(user, secret, method)
387
+ return self
388
+ end
389
+ end
390
+
391
+ def do_start(user, secret, method) #:nodoc:
392
+ raise IOError, 'NNTP session already started' if @started
393
+ check_auth_args user, secret, method if user or secret
394
+
395
+ if InternetMessageIO.respond_to?(:old_open)
396
+ @socket = InternetMessageIO.old_open(@address, @port, @open_timeout,
397
+ @read_timeout, @debug_output)
398
+
399
+ else
400
+ @socket = InternetMessageIO.open(@address, @port, @open_timeout,
401
+ @read_timeout, @debug_output)
402
+ end
403
+
404
+ check_response(critical { recv_response() })
405
+
406
+ mode_reader_success = false
407
+ tried_authenticating = false
408
+ until mode_reader_success
409
+ begin
410
+ mode_reader
411
+ mode_reader_success = true
412
+ rescue NNTPAuthenticationError
413
+ if tried_authenticating
414
+ raise
415
+ end
416
+ rescue ProtocolError
417
+ raise
418
+ end
419
+ authenticate user, secret, method if user
420
+ tried_authenticating = true
421
+ end
422
+
423
+ authenticate user, secret, method if user
424
+ @started = true
425
+ ensure
426
+ @socket.close if not @started and @socket and not @socket.closed?
427
+ end
428
+ private :do_start
429
+
430
+ # Finishes the NNTP session and closes TCP connection. Raises IOError if
431
+ # not started.
432
+ def finish
433
+ raise IOError, 'not yet started' unless started?
434
+ do_finish
435
+ end
436
+
437
+ def do_finish #:nodoc:
438
+ quit if @socket and not @socket.closed? and not @error_occured
439
+ ensure
440
+ @started = false
441
+ @error_occured = false
442
+ @socket.close if @socket and not @socket.closed?
443
+ @socket = nil
444
+ end
445
+ private :do_finish
446
+
447
+ public
448
+
449
+ # POST
450
+ #
451
+ # Posts +msgstr+ as a message. Single CR ("\r") and LF ("\n") found in
452
+ # the +msgstr+, are converted into the CR LF pair. You cannot post a
453
+ # binary message with this method. +msgstr+ _should include both the
454
+ # message headers and body_. All non US-ASCII, binary and, or multi-part
455
+ # messages should be submitted in an encoded form as per MIME standards.
456
+ #
457
+ # === Example
458
+ #
459
+ # Net::NNTP.start('nntp.example.com') do |nntp|
460
+ # nntp.post msgstr
461
+ # end
462
+ #
463
+ # === Errors
464
+ #
465
+ # This method may raise:
466
+ #
467
+ # * Net::NNTPFatalError
468
+ # * Net::NNTPPostingNotAllowed
469
+ # * Net::NNTPServerBusy
470
+ # * Net::NNTPSyntaxError
471
+ # * Net::NNTPUnknownError
472
+ # * IOError
473
+ # * TimeoutError
474
+ #
475
+ def post(msgstr)
476
+ stat = post0 {
477
+ @socket.write_message msgstr
478
+ }
479
+ return stat[0..3], stat[4..-1].chop
480
+ end
481
+
482
+ # Opens a message writer stream and gives it to the block. The stream is
483
+ # valid only in the block, and has these methods:
484
+ #
485
+ # puts(str = ''):: outputs STR and CR LF.
486
+ # print(str):: outputs STR.
487
+ # printf(fmt, *args):: outputs sprintf(fmt,*args).
488
+ # write(str):: outputs STR and returns the length of written bytes.
489
+ # <<(str):: outputs STR and returns self.
490
+ #
491
+ # If a single CR ("\r") or LF ("\n") is found in the message, it is
492
+ # converted to the CR LF pair. You cannot post a binary message with
493
+ # this method.
494
+ #
495
+ # === Parameters
496
+ #
497
+ # Block
498
+ #
499
+ # === Example
500
+ #
501
+ # Net::NNTP.start('nntp.example.com', 119) do |nntp|
502
+ # nntp.open_message_stream do |f|
503
+ # f.puts 'From: from@example.com'
504
+ # f.puts 'Newsgroups: news.group.one, news.group.two ...'
505
+ # f.puts 'Subject: test message'
506
+ # f.puts
507
+ # f.puts 'This is a test message.'
508
+ # end
509
+ # end
510
+ #
511
+ # === Errors
512
+ #
513
+ # This method may raise:
514
+ #
515
+ # * Net::NNTPFatalError
516
+ # * Net::NNTPPostingNotAllowed
517
+ # * Net::NNTPServerBusy
518
+ # * Net::NNTPSyntaxError
519
+ # * Net::NNTPUnknownError
520
+ # * IOError
521
+ # * TimeoutError
522
+ #
523
+ def open_message_stream(&block) # :yield: stream
524
+ post0 { @socket.write_message_by_block(&block) }
525
+ end
526
+
527
+ # ARTICLE [<Message-ID>|<Number>]
528
+ def article(id_num = nil)
529
+ stat, text = longcmd("ARTICLE #{id_num}".strip)
530
+ return stat[0..2], text
531
+ end
532
+
533
+ # BODY [<Message-ID>|<Number>]
534
+ def body(id_num = nil)
535
+ stat, text = longcmd("BODY #{id_num}".strip)
536
+ return stat[0..2], text
537
+ end
538
+
539
+ # IO_BODY <output IO object> [<Message-ID>|<Number>]
540
+ def io_body (io_output, id_num = nil)
541
+ stat = io_longcmd(io_output, "BODY #{id_num}".strip)
542
+ return stat[0..2], io_output
543
+ end
544
+
545
+ # DATE
546
+ def date
547
+ text = []
548
+ stat = shortcmd("DATE")
549
+ text << stat[4...12]
550
+ text << stat[12...18]
551
+ raise NNTPDataError, stat, caller unless text[0].length == 8 and text[1].length == 6
552
+ return stat[0..2], text
553
+ end
554
+
555
+ # GROUP <Newsgroup>
556
+ def group(ng)
557
+ stat = shortcmd("GROUP %s", ng)
558
+ return stat[0..2], stat[4..-1].chop
559
+ end
560
+
561
+ # HEAD [<Message-ID>|<Number>]
562
+ def head(id_num = nil)
563
+ stat, text = longcmd("HEAD #{id_num}".strip)
564
+ return stat[0..2], text
565
+ end
566
+
567
+ # HELP
568
+ def help
569
+ stat, text = longcmd('HELP')
570
+ text.each_with_index do |line, index|
571
+ text[index] = line.gsub(/\A\s+/, '')
572
+ end
573
+ return stat[0..2], text
574
+ end
575
+
576
+ # LAST
577
+ def last
578
+ stat = shortcmd('LAST')
579
+ return stat[0..2], stat[4..-1].chop
580
+ end
581
+
582
+ # LIST [ACTIVE|NEWSGROUPS] [<Wildmat>]]:br:
583
+ # LIST [ACTIVE.TIMES|EXTENSIONS|SUBSCRIPTIONS|OVERVIEW.FMT]
584
+ def list(opts = nil)
585
+ stat, text = longcmd("LIST #{opts}".strip)
586
+ return stat[0..2], text
587
+ end
588
+
589
+ # LISTGROUP <Newsgroup>
590
+ def listgroup(ng)
591
+ stat, text = longcmd("LISTGROUP #{ng}".strip)
592
+ return stat[0..2], text
593
+ end
594
+
595
+ # MODE READER
596
+ def mode_reader
597
+ stat = shortcmd('MODE READER')
598
+ return stat[0..2], stat[4..-1].chop
599
+ end
600
+ private :mode_reader #:nodoc:
601
+
602
+ # NEWGROUPS <yymmdd> <hhmmss> [GMT]
603
+ def newgroups(date, time, tzone = nil)
604
+ stat, text = longcmd("NEWGROUPS #{date} #{time} #{tzone}".strip)
605
+ return stat[0..2], text
606
+ end
607
+
608
+ # NEXT
609
+ def next
610
+ stat = shortcmd('NEXT')
611
+ return stat[0..2], stat[4..-1].chop
612
+ end
613
+
614
+ # OVER <Range> # e.g first[-[last]]
615
+ def over(range)
616
+ stat, text = longcmd("OVER #{range}".strip)
617
+ return stat[0..2], text
618
+ end
619
+
620
+ # QUIT
621
+ def quit
622
+ stat = shortcmd('QUIT')
623
+ end
624
+ private :quit #:nodoc:
625
+
626
+ # SLAVE
627
+ def slave
628
+ stat = shortcmd('SLAVE')
629
+ return stat[0..2], stat[4..-1].chop
630
+ end
631
+
632
+ # STAT [<Message-ID>|<Number>]
633
+ def stat(id_num = nil)
634
+ stat = shortcmd("STAT #{id_num}".strip)
635
+ return stat[0..2], stat[4..-1].chop
636
+ end
637
+
638
+ # XHDR <Header> <Message-ID>|<Range> # e.g first[-[last]]
639
+ def xhdr(header, id_range)
640
+ stat, text = longcmd("XHDR #{header} #{id_range}".strip)
641
+ return stat[0..2], text
642
+ end
643
+
644
+ # XOVER <Range> # e.g first[-[last]]
645
+ def xover(range)
646
+ stat, text = longcmd("XOVER #{range}".strip)
647
+ return stat[0..2], text
648
+ end
649
+
650
+ private
651
+
652
+ #
653
+ # row level library
654
+ #
655
+
656
+ def post0
657
+ raise IOError, 'closed session' unless @socket
658
+ stat = critical {
659
+ check_response(get_response('POST'), true)
660
+ yield
661
+ recv_response()
662
+ }
663
+ check_response(stat)
664
+ end
665
+
666
+ #
667
+ # auth
668
+ #
669
+
670
+ def check_auth_args(user, secret, method)
671
+ raise ArgumentError, 'both user and secret are required'\
672
+ unless user and secret
673
+ authmeth = "auth_#{method || 'original'}"
674
+ raise ArgumentError, "wrong auth type #{method}"\
675
+ unless respond_to?(authmeth, true)
676
+ end
677
+
678
+ def authenticate(user, secret, method)
679
+ methods = %w(original simple generic plain starttls external cram_md5 digest_md5 gassapi)
680
+ method = "#{method || 'original'}"
681
+ authmeth = methods.index(method)
682
+ begin
683
+ __send__("auth_#{method}", user, secret)
684
+ rescue NNTPAuthenticationError
685
+ if authmeth and authmeth > 0
686
+ authmeth -= 1 # fallback
687
+ method = methods[authmeth]
688
+ @error_occured = false
689
+ retry
690
+ else
691
+ raise
692
+ end
693
+ end
694
+ end
695
+
696
+ # AUTHINFO USER username
697
+ # AUTHINFO PASS password
698
+ def auth_original(user, secret)
699
+ stat = critical {
700
+ check_response(get_response("AUTHINFO USER %s", user), true)
701
+ check_response(get_response("AUTHINFO PASS %s", secret), true)
702
+ }
703
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
704
+ end
705
+
706
+ # AUTHINFO SIMPLE
707
+ # username password
708
+ def auth_simple(user, secret)
709
+ stat = critical {
710
+ check_response(get_response('AUTHINFO SIMPLE'), true)
711
+ check_response(get_response('%s %s', user, secret), true)
712
+ }
713
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
714
+ end
715
+
716
+ # AUTHINFO GENERIC authenticator arguments ...
717
+ #
718
+ # The authentication protocols are not inculeded in RFC2980,
719
+ # see [RFC1731] (http://www.ieft.org/rfc/rfc1731.txt).
720
+ def auth_generic(fmt, *args)
721
+ stat = critical {
722
+ cmd = 'AUTHINFO GENERIC ' + sprintf(fmt, *args)
723
+ check_response(get_response(cmd), true)
724
+ }
725
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
726
+ end
727
+
728
+ # AUTHINFO SASL PLAIN
729
+ def auth_plain(user, secret)
730
+ stat = critical {
731
+ check_response(get_response('AUTHINFO SASL PLAIN %s',
732
+ base64_encode("\0#{user}\0#{secret}")), true)
733
+ }
734
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
735
+ end
736
+
737
+ # STARTTLS
738
+ def auth_starttls(user, secret)
739
+ stat = critical {
740
+ check_response(get_response('STARTTLS'), true)
741
+ ### FIXME:
742
+ }
743
+ raise NNTPAuthenticationError, 'not implemented as yet!'
744
+ end
745
+
746
+ # AUTHINFO SASL EXTERNAL =
747
+ def auth_external(user, secret)
748
+ stat = critical {
749
+ check_response(get_response('AUTHINFO SASL EXTERNAL ='), true)
750
+ ### FIXME:
751
+ }
752
+ raise NNTPAuthenticationError, 'not implemented as yet!'
753
+ end
754
+
755
+ # AUTHINFO SASL CRAM-MD5 [RFC2195]
756
+ def auth_cram_md5(user, secret)
757
+ stat = nil
758
+ critical {
759
+ stat = check_response(get_response('AUTHINFO SASL CRAM-MD5'), true)
760
+ challenge = stat.split(/ /)[1].unpack('m')[0]
761
+ secret = Digest::MD5.digest(secret) if secret.size > 64
762
+
763
+ isecret = secret + "\0" * (64 - secret.size)
764
+ osecret = isecret.dup
765
+ 0.upto(63) do |i|
766
+ isecret[i] ^= 0x36
767
+ osecret[i] ^= 0x5c
768
+ end
769
+ tmp = Digest::MD5.digest(isecret + challenge)
770
+ tmp = Digest::MD5.hexdigest(osecret + tmp)
771
+
772
+ stat = get_response(base64_encode(user + ' ' + tmp))
773
+ }
774
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
775
+ end
776
+
777
+ # AUTHINFO SASL DIGEST-MD5
778
+ def auth_digest_md5(user, secret)
779
+ stat = critical {
780
+ check_response(get_response('AUTHINFO SASL DIGEST-MD5'), true)
781
+ ### FIXME:
782
+ }
783
+ raise NNTPAuthenticationError, 'not implemented as yet!'
784
+ end
785
+
786
+ # AUTHINFO SASL GASSAPI
787
+ def auth_gassapi(user, secret)
788
+ stat = critical {
789
+ check_response(get_response('AUTHINFO SASL GASSAPI'), true)
790
+ ### FIXME:
791
+ }
792
+ raise NNTPAuthenticationError, 'not implemented as yet!'
793
+ end
794
+
795
+ def base64_encode(str)
796
+ # expects "str" may not become too long
797
+ [str].pack('m').gsub(/\s+/, '')
798
+ end
799
+
800
+ def longcmd(fmt, *args)
801
+ text = []
802
+ stat = io_longcmd(text, fmt, *args)
803
+ return stat, text.map { |line| line.chomp! }
804
+ end
805
+
806
+ def io_longcmd(target, fmt, *args)
807
+ if stat = shortcmd(fmt, *args)
808
+ while true
809
+ line = @socket.readline
810
+ break if line =~ /^\.\s*$/ # done
811
+ line = line[1..-1] if line.to_s[0...2] == '..'
812
+ target << line + $/
813
+ end
814
+ end
815
+
816
+ return stat, target
817
+ end
818
+
819
+ def shortcmd(fmt, *args)
820
+ stat = critical {
821
+ @socket.writeline sprintf(fmt, *args)
822
+ recv_response()
823
+ }
824
+ check_response(stat)
825
+ end
826
+
827
+ def get_response(fmt, *args)
828
+ @socket.writeline sprintf(fmt, *args)
829
+ recv_response()
830
+ end
831
+
832
+ def recv_response
833
+ stat = ''
834
+ while true
835
+ line = @socket.readline
836
+ stat << line << "\n"
837
+ break unless line[3] == ?- # "210-PIPELINING"
838
+ end
839
+ stat
840
+ end
841
+
842
+ def check_response(stat, allow_continue = false)
843
+ return stat if /\A1/ === stat # 1xx info msg
844
+ return stat if /\A2/ === stat # 2xx cmd k
845
+ return stat if allow_continue and /\A[35]/ === stat # 3xx cmd k, snd rst
846
+ exception = case stat
847
+ when /\A440/ then NNTPPostingNotAllowed # 4xx cmd k, bt nt prfmd
848
+ when /\A48/ then NNTPAuthenticationError
849
+ when /\A4/ then NNTPServerBusy
850
+ when /\A50/ then NNTPSyntaxError # 5xx cmd ncrrct
851
+ when /\A55/ then NNTPFatalError
852
+ else
853
+ NNTPUnknownError
854
+ end
855
+ raise exception, stat
856
+ end
857
+
858
+ def critical(&block)
859
+ return '200 dummy reply code' if @error_occured
860
+ begin
861
+ return yield()
862
+ rescue Exception
863
+ @error_occured = true
864
+ raise
865
+ end
866
+ end
867
+
868
+ end # class_NNTP
869
+
870
+ NNTPSession = NNTP
871
+ end # module_Net