thoughtafter-nntp 1.0.0.2 → 1.0.0.3

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