entp-astrotrain 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/.gitignore +26 -0
  2. data/LICENSE +20 -0
  3. data/README +47 -0
  4. data/Rakefile +145 -0
  5. data/VERSION +1 -0
  6. data/astrotrain.gemspec +96 -0
  7. data/config/sample.rb +12 -0
  8. data/lib/astrotrain/api.rb +53 -0
  9. data/lib/astrotrain/logged_mail.rb +41 -0
  10. data/lib/astrotrain/mapping/http_post.rb +18 -0
  11. data/lib/astrotrain/mapping/jabber.rb +23 -0
  12. data/lib/astrotrain/mapping/transport.rb +55 -0
  13. data/lib/astrotrain/mapping.rb +157 -0
  14. data/lib/astrotrain/message.rb +313 -0
  15. data/lib/astrotrain/tmail.rb +48 -0
  16. data/lib/astrotrain.rb +55 -0
  17. data/lib/vendor/rest-client/README.rdoc +104 -0
  18. data/lib/vendor/rest-client/Rakefile +84 -0
  19. data/lib/vendor/rest-client/bin/restclient +65 -0
  20. data/lib/vendor/rest-client/foo.diff +66 -0
  21. data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +21 -0
  22. data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
  23. data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
  24. data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
  25. data/lib/vendor/rest-client/lib/rest_client.rb +189 -0
  26. data/lib/vendor/rest-client/rest-client.gemspec +18 -0
  27. data/lib/vendor/rest-client/spec/base.rb +5 -0
  28. data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
  29. data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
  30. data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
  31. data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
  32. data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
  33. data/tasks/doc.thor +149 -0
  34. data/tasks/merb.thor +2020 -0
  35. data/test/api_test.rb +28 -0
  36. data/test/fixtures/apple_multipart.txt +100 -0
  37. data/test/fixtures/basic.txt +14 -0
  38. data/test/fixtures/custom.txt +15 -0
  39. data/test/fixtures/fwd.txt +0 -0
  40. data/test/fixtures/gb2312_encoding.txt +16 -0
  41. data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
  42. data/test/fixtures/html.txt +16 -0
  43. data/test/fixtures/iso-8859-1.txt +13 -0
  44. data/test/fixtures/mapped.txt +13 -0
  45. data/test/fixtures/multipart.txt +213 -0
  46. data/test/fixtures/multipart2.txt +213 -0
  47. data/test/fixtures/multiple.txt +13 -0
  48. data/test/fixtures/multiple_delivered_to.txt +14 -0
  49. data/test/fixtures/multiple_with_body_recipients.txt +15 -0
  50. data/test/fixtures/reply.txt +16 -0
  51. data/test/fixtures/utf-8.txt +13 -0
  52. data/test/logged_mail_test.rb +63 -0
  53. data/test/mapping_test.rb +129 -0
  54. data/test/message_test.rb +424 -0
  55. data/test/test_helper.rb +54 -0
  56. data/test/transport_test.rb +111 -0
  57. metadata +115 -0
@@ -0,0 +1,157 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ include DataMapper::Resource
4
+
5
+ class << self
6
+ attr_accessor :default_domain
7
+ attr_accessor :transports
8
+ end
9
+
10
+ self.transports = {"HTTP Post" => 'http_post', "Jabber" => 'jabber'}
11
+ self.default_domain = 'astrotrain.com'
12
+
13
+ property :id, Serial
14
+ property :email_user, String, :size => 255, :length => 1..255, :index => :email, :format => /^[\w\.\_\%\+\-]*\*?$/
15
+ property :email_domain, String, :size => 255, :lenght => 1..255, :index => :email, :format => /^[\w\-\_\.]+$/, :default => lambda { default_domain }
16
+ property :destination, String, :size => 255, :length => 1..255
17
+ property :transport, String, :size => 255, :set => transports.values, :default => 'http_post'
18
+ property :separator, String, :size => 255
19
+
20
+ validates_is_unique :email_user, :scope => :email_domain
21
+ validates_format :destination, :as => /^(https?:)\/\/[^\/]+\/?/i, :if => :destination_uses_url?
22
+ validates_format :destination, :as => :email_address, :if => :destination_uses_email?
23
+
24
+ has n, :logged_mails, :order => [:created_at.desc]
25
+
26
+ # returns a mapping for the given array of email addresses
27
+ def self.match(email_addresses)
28
+ email_addresses.each do |email_address|
29
+ email_address.strip!
30
+ email_address.downcase!
31
+ name, domain = email_address.split("@")
32
+ if mapping = match_by_address(name, domain) || match_by_wildcard(name, domain)
33
+ return [mapping, email_address]
34
+ end
35
+ end
36
+ nil
37
+ end
38
+
39
+ # Processes a given message. It finds a mapping, creates a LoggedMail record,
40
+ # and attempts to process the message.
41
+ def self.process(message)
42
+ LoggedMail.from(message) do |logged|
43
+ save_logged = begin
44
+ mapping, recipient = match(message.recipients)
45
+ if mapping
46
+ logged.recipient = recipient
47
+ logged.mapping = mapping
48
+ mapping.process(message, recipient)
49
+ logged.delivered_at = Time.now.utc
50
+ end
51
+ LoggedMail.log_processed # save successfully processed messages?
52
+ rescue
53
+ logged.error_message = "#{$!.class}: #{$!}"
54
+ end
55
+ Astrotrain.callback(:post_processing, message, mapping, logged)
56
+ save_logged
57
+ end
58
+ end
59
+
60
+ # Processes a given message and recipient against the mapping's transport.
61
+ def process(message, recipient)
62
+ Astrotrain.callback(:pre_processing, message, self)
63
+ Transport.process(message, self, recipient)
64
+ end
65
+
66
+ # returns true if the email matches this mapping. Wildcards in the name are allowed.
67
+ # A mapping with foo*@bar.com will match foo@bar.com and food@bar.com, but not foo@baz.com.
68
+ def match?(name, domain)
69
+ email_domain == domain && name =~ email_user_regex
70
+ end
71
+
72
+ def destination_uses_url?
73
+ transport == 'http_post'
74
+ end
75
+
76
+ def destination_uses_email?
77
+ transport == 'jabber'
78
+ end
79
+
80
+ def full_email
81
+ "#{email_user}@#{email_domain}"
82
+ end
83
+
84
+ # Looks for the mapping's separator in the message body and pulls only the content
85
+ # above it. Assuming a separator of '===='...
86
+ #
87
+ # This will be kept
88
+ #
89
+ # On Thu, Sep 3, 2009 at 12:34 AM... (this will be removed)
90
+ # ====
91
+ #
92
+ # > Everything here will be removed.
93
+ #
94
+ def find_reply_from(body)
95
+ return if separator.blank?
96
+ return '' if body.blank?
97
+ lines = body.split("\n")
98
+ delim_line = found_empty = nil
99
+
100
+ (lines.size - 1).downto(0) do |i|
101
+ line = lines[i]
102
+ if !delim_line && line.include?(separator)
103
+ delim_line = i
104
+ elsif delim_line && !found_empty
105
+ delim_line = i
106
+ found_empty = line.strip.blank?
107
+ elsif delim_line && found_empty
108
+ if date_reply_line?(line) || line.strip.blank?
109
+ delim_line = i
110
+ else
111
+ break
112
+ end
113
+ end
114
+ end
115
+
116
+ if delim_line
117
+ body = if delim_line.zero?
118
+ []
119
+ elsif lines.size >= delim_line
120
+ lines[0..delim_line-1]
121
+ else
122
+ lines
123
+ end.join("\n")
124
+ elsif body.frozen?
125
+ body = body.dup
126
+ end
127
+ body.strip!
128
+ body
129
+ end
130
+
131
+ protected
132
+ def self.match_by_address(name, domain)
133
+ first(:email_user => name, :email_domain => domain)
134
+ end
135
+
136
+ def self.match_by_wildcard(name, domain)
137
+ wildcards = all(:email_domain => domain, :email_user.like => "%*")
138
+ wildcards.sort! { |x, y| y.email_user.size <=> x.email_user.size }
139
+ wildcards.detect { |w| w.match?(name, domain) }
140
+ end
141
+
142
+ DATE_LANGUATE_REGEXES = [/^on\b.*wrote\b?:$/i, /^am\b.*schrieb [\w\d\s]+:$/i, /^le\b.*a écrit\b?:$/i]
143
+ def date_reply_line?(line)
144
+ DATE_LANGUATE_REGEXES.any? { |re| line =~ re }
145
+ end
146
+
147
+ def email_user_regex
148
+ @email_user_regex ||= begin
149
+ if email_user['*']
150
+ /^#{email_user.sub /\*/, '(.*)'}$/
151
+ else
152
+ /^#{email_user}$/
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,313 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'set'
5
+
6
+ module Astrotrain
7
+ # Wrapper around a TMail object
8
+ class Message
9
+ attr_accessor :body
10
+ attr_reader :mail, :attachments
11
+
12
+ class << self
13
+ attr_reader :queue_path, :archive_path
14
+ attr_accessor :recipient_header_order, :skipped_headers
15
+ end
16
+
17
+ def self.queue_path=(path)
18
+ if path
19
+ path = File.expand_path(path)
20
+ FileUtils.mkdir_p path
21
+ end
22
+ @queue_path = path
23
+ end
24
+
25
+ def self.archive_path=(path)
26
+ if path
27
+ path = File.expand_path(path)
28
+ FileUtils.mkdir_p path
29
+ end
30
+ @archive_path = path
31
+ end
32
+
33
+ self.skipped_headers = Set.new %w(date from subject delivered-to x-original-to received)
34
+ self.recipient_header_order = %w(original_to delivered_to to)
35
+ self.queue_path = File.join(Astrotrain.root, 'queue')
36
+
37
+ # Dumps the raw text into the queue_path. Not really recommended, since you should
38
+ # set the queue_path to the directory your incoming emails are dumped into.
39
+ def self.queue(raw)
40
+ filename = nil
41
+ digest = Digest::SHA1.hexdigest(raw)
42
+ while filename.nil? || File.exist?(filename)
43
+ filename = File.join(queue_path, Digest::SHA1.hexdigest(digest + rand.to_s))
44
+ end
45
+ File.open filename, 'wb' do |f|
46
+ f.write raw
47
+ end
48
+ filename
49
+ end
50
+
51
+ # Parses the given raw email text and processes it with a matching Mapping.
52
+ def self.receive(raw)
53
+ message = parse(raw)
54
+ Astrotrain.callback(:pre_mapping, message)
55
+ Mapping.process(message)
56
+ message
57
+ end
58
+
59
+ # Processes the given file. It parses it by reading the contents, and optionally
60
+ # archives or removes the original file.
61
+ def self.receive_file(path, raw = nil)
62
+ message = receive IO.read(path)
63
+ if archive_path
64
+ daily_archive_path = archive_path / Time.now.year.to_s / Time.now.month.to_s / Time.now.day.to_s
65
+ FileUtils.mkdir_p(daily_archive_path)
66
+ FileUtils.mv path, daily_archive_path / File.basename(path)
67
+ else
68
+ FileUtils.rm_rf path
69
+ end
70
+ message
71
+ end
72
+
73
+ # Parses the raw email headers into a Astrotrain::Message instance.
74
+ def self.parse(raw)
75
+ new Mail.parse(raw)
76
+ end
77
+
78
+ def self.parse_email_addresses(value)
79
+ emails = value.split(",")
80
+ collection = []
81
+ emails.each do |addr|
82
+ addr.strip!
83
+ next if addr.blank?
84
+ header = parse_email_address(addr.to_s)
85
+ collection << unescape(header[:email]) if !header[:email].blank?
86
+ end
87
+ collection
88
+ end
89
+
90
+ def self.parse_email_address(email)
91
+ return {} if email.blank?
92
+ begin
93
+ header = TMail::Address.parse(email)
94
+ {:name => header.name, :email => header.address}
95
+ rescue SyntaxError, TMail::SyntaxError
96
+ email = email.scan(/\<([^\>]+)\>/)[0]
97
+ if email.blank?
98
+ return {:name => nil, :email => nil}
99
+ else
100
+ email = email[0]
101
+ retry
102
+ end
103
+ end
104
+ end
105
+
106
+ # Stolen from Rack/Camping, remove the "+" => " " translation
107
+ def self.unescape(s)
108
+ s.gsub!(/((?:%[0-9a-fA-F]{2})+)/n){
109
+ [$1.delete('%')].pack('H*')
110
+ }
111
+ s
112
+ end
113
+
114
+ def initialize(mail)
115
+ @mail = mail
116
+ @mapping = nil
117
+ @attachments = []
118
+ @recipients = {}
119
+ end
120
+
121
+ # Gets the recipients of an email using the To/Delivered-To/X-Original-To headers.
122
+ # It's not always straightforward which email we want when dealing with filters
123
+ # and forward rules.
124
+ def recipients(order = nil)
125
+ if !@recipients.key?(order)
126
+ order = self.class.recipient_header_order if order.blank?
127
+ recipients = []
128
+
129
+ parse_email_headers recipients_from_body, recipients
130
+ order.each do |key|
131
+ parse_email_headers(send("recipients_from_#{key}"), recipients)
132
+ end
133
+
134
+ recipients.flatten!
135
+ recipients.uniq!
136
+ @recipients[order] = recipients
137
+ else
138
+ @recipients[order]
139
+ end
140
+ end
141
+
142
+ def recipients_from_to
143
+ @recipient_from_to ||= [@mail['to'].to_s]
144
+ end
145
+
146
+ def recipients_from_delivered_to
147
+ @recipient_from_delivered_to ||= begin
148
+ delivered = @mail['Delivered-To']
149
+ if delivered.respond_to?(:first)
150
+ delivered.map! { |a| a.to_s }
151
+ else
152
+ [delivered.to_s]
153
+ end
154
+ end
155
+ end
156
+
157
+ def recipients_from_original_to
158
+ @recipient_from_original_to ||= [@mail['X-Original-To'].to_s]
159
+ end
160
+
161
+ def recipients_from_body
162
+ @recipients_from_body ||= body.scan(/<[\w\.\_\%\+\-]+@[\w\-\_\.]+>/)
163
+ end
164
+
165
+ def sender
166
+ @sender ||= TMail::Unquoter.unquote_and_convert_to(@mail['from'].to_s, "utf-8")
167
+ end
168
+
169
+ def subject
170
+ @mail.subject
171
+ rescue Iconv::InvalidCharacter
172
+ @mail.quoted_subject
173
+ end
174
+
175
+ def body
176
+ @body ||= begin
177
+ process_message_body
178
+ @body
179
+ end
180
+ end
181
+
182
+ def html
183
+ @html ||= begin
184
+ process_message_body
185
+ @html
186
+ end
187
+ end
188
+
189
+ def raw
190
+ @mail.port.to_s
191
+ end
192
+
193
+ def headers
194
+ @headers ||= begin
195
+ h = {}
196
+ @mail.header.each do |key, value|
197
+ next if self.class.skipped_headers.include?(key)
198
+ h[key] = value.to_s
199
+ end
200
+ h
201
+ end
202
+ end
203
+
204
+ class Attachment
205
+ def initialize(part)
206
+ @part = part
207
+ @is_read = false
208
+ end
209
+
210
+ def content_type
211
+ @part.content_type
212
+ end
213
+
214
+ def filename
215
+ @filename ||= @part.type_param("name") || @part.disposition_param('filename')
216
+ end
217
+
218
+ # For IO API compatibility when used with Rest-Client
219
+ def close
220
+ end
221
+
222
+ alias path filename
223
+
224
+ def read(value = nil)
225
+ if read?
226
+ nil
227
+ else
228
+ @is_read = true
229
+ data
230
+ end
231
+ end
232
+
233
+ def read?
234
+ @is_read == true
235
+ end
236
+
237
+ def data
238
+ @part.body
239
+ end
240
+
241
+ def attached?
242
+ !filename.nil?
243
+ end
244
+
245
+ def ==(other)
246
+ super || (filename == other.filename && content_type == other.content_type)
247
+ end
248
+
249
+ def inspect
250
+ %(#<Message::Attachment filename=#{filename.inspect} content_type=#{content_type.inspect}>)
251
+ end
252
+ end
253
+
254
+ protected
255
+ def process_message_body
256
+ if @mail.multipart?
257
+ @attachments.clear
258
+ @body, @html = [], []
259
+ scan_parts(@mail)
260
+ @body = @body.join("\n")
261
+ @html = @html.join("\n")
262
+ else
263
+ @body = @mail.body
264
+ @html = ''
265
+ end
266
+ if !@mail.charset
267
+ @body = convert_to_utf8(@body)
268
+ @html = convert_to_utf8(@html)
269
+ end
270
+ end
271
+
272
+ def scan_parts(message)
273
+ message.parts.each do |part|
274
+ if part.multipart?
275
+ scan_parts(part)
276
+ else
277
+ case part.content_type
278
+ when 'text/plain'
279
+ @body << part.body
280
+ when 'text/html'
281
+ @html << part.body
282
+ else
283
+ att = Attachment.new(part)
284
+ @attachments << att if att.attached?
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ def parse_email_headers(values, collection)
291
+ values.each do |value|
292
+ if !value.blank?
293
+ collection.push *self.class.parse_email_addresses(value)
294
+ end
295
+ end
296
+ end
297
+
298
+ # Attempts to run iconv conversions in common charsets to UTF-8. Needed for
299
+ # those crappy emails that don't properly specify a charset in the headers.
300
+ ICONV_CONVERSIONS = %w(utf-8 ISO-8859-1 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 ISO-8859-7 ISO-8859-8 ISO-8859-9
301
+ ISO-8859-15 GB2312)
302
+ def convert_to_utf8(s)
303
+ ICONV_CONVERSIONS.each do |from|
304
+ begin
305
+ return Iconv.iconv(ICONV_CONVERSIONS[0], from, s).to_s
306
+ rescue Iconv::IllegalSequence
307
+ ensure
308
+ s
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,48 @@
1
+ module Astrotrain
2
+ # custom subclass of TMail::Mail that fixes some bugs. The fixes were pushed upstream,
3
+ # and this class will go away once the gem is released.
4
+ class Mail < TMail::Mail
5
+ def charset( default = nil )
6
+ if h = @header['content-type']
7
+ h['charset'] || mime_version_charset || default
8
+ else
9
+ mime_version_charset || default
10
+ end
11
+ end
12
+
13
+ # some weird emails come with the charset specified in the mime-version header:
14
+ #
15
+ # #<TMail::MimeVersionHeader "1.0\n charset=\"gb2312\"">
16
+ #
17
+ def mime_version_charset
18
+ if header['mime-version'].inspect =~ /charset=('|\\")?([^\\"']+)/
19
+ $2
20
+ end
21
+ end
22
+
23
+ # copied from TMail::Mail, uses #charset instead of #sub_header
24
+ def unquoted_body(to_charset = 'utf-8')
25
+ from_charset = charset
26
+ case (content_transfer_encoding || "7bit").downcase
27
+ when "quoted-printable"
28
+ # the default charset is set to iso-8859-1 instead of 'us-ascii'.
29
+ # This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
30
+ if !from_charset.blank? && from_charset.downcase == 'us-ascii'
31
+ from_charset = 'iso-8859-1'
32
+ end
33
+
34
+ TMail::Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
35
+ to_charset, from_charset, true)
36
+ when "base64"
37
+ TMail::Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
38
+ from_charset)
39
+ when "7bit", "8bit"
40
+ TMail::Unquoter.convert_to(quoted_body, to_charset, from_charset)
41
+ when "binary"
42
+ quoted_body
43
+ else
44
+ quoted_body
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/astrotrain.rb ADDED
@@ -0,0 +1,55 @@
1
+ module Astrotrain
2
+ CALLBACK_TYPES = [:pre_mapping, :pre_processing, :post_processing]
3
+ class << self
4
+ attr_accessor :root, :lib_root, :callbacks
5
+ end
6
+
7
+ def self.load(root = Dir.pwd)
8
+ self.root = File.expand_path(root)
9
+ self.lib_root = File.expand_path(File.dirname(__FILE__))
10
+ load_dependencies
11
+ yield if block_given?
12
+ %w(tmail message mapping logged_mail mapping/transport mapping/http_post mapping/jabber).each do |lib|
13
+ require "astrotrain/#{lib}"
14
+ end
15
+ Astrotrain::Mail::ALLOW_MULTIPLE['delivered-to'] = true
16
+ end
17
+
18
+ def self.callback(name, *args, &block)
19
+ found = callbacks[name]
20
+ if block
21
+ found << block
22
+ else
23
+ found.each { |cback| cback.call(*args) }
24
+ end
25
+ found
26
+ end
27
+
28
+ def self.clear_callbacks
29
+ self.callbacks = CALLBACK_TYPES.inject({}) { |memo, ctype| memo.update(ctype => []) }
30
+ end
31
+
32
+ clear_callbacks
33
+
34
+ private
35
+ # help me ryan tomayko, you're my only help
36
+ def self.load_dependencies
37
+ require 'rubygems'
38
+ gem 'addressable', '2.0.2'
39
+ gem "tmail", "1.2.3.1"
40
+ gem "xmpp4r-simple", "0.8.8"
41
+
42
+ dm_ver = "0.9.11"
43
+ gem "dm-core", dm_ver # The datamapper ORM
44
+ gem "dm-aggregates", dm_ver # Provides your DM models with count, sum, avg, min, max, etc.
45
+ gem "dm-timestamps", dm_ver # Automatically populate created_at, created_on, etc. when those properties are present.
46
+ gem "dm-types", dm_ver # Provides additional types, including csv, json, yaml.
47
+ gem "dm-validations", dm_ver # Validation framework
48
+
49
+ $LOAD_PATH.unshift File.join(lib_root, 'vendor', 'rest-client', 'lib')
50
+
51
+ %w(dm-core dm-aggregates dm-timestamps dm-types dm-validations xmpp4r-simple tmail rest_client).each do |lib|
52
+ require lib
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ = REST Client -- simple DSL for accessing REST resources
2
+
3
+ A simple REST client for Ruby, inspired by the Sinatra's microframework style
4
+ of specifying actions: get, put, post, delete.
5
+
6
+ == Usage: Raw URL
7
+
8
+ require 'rest_client'
9
+
10
+ RestClient.get 'http://example.com/resource'
11
+ RestClient.get 'https://user:password@example.com/private/resource'
12
+
13
+ RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
14
+
15
+ RestClient.delete 'http://example.com/resource'
16
+
17
+ == Multipart
18
+
19
+ Yeah, that's right! This does multipart sends for you!
20
+
21
+ RestClient.post '/data', :myfile => File.new("/path/to/image.jpg")
22
+
23
+ This does two things for you:
24
+
25
+ * Auto-detects that you have a File value sends it as multipart
26
+ * Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
27
+
28
+ If you are sending params that do not contain a File object but the payload needs to be multipart then:
29
+
30
+ RestClient.post '/data', :foo => 'bar', :multipart => true
31
+
32
+ == Streaming downloads
33
+
34
+ RestClient.get('http://some/resource/lotsofdata') do |res|
35
+ res.read_body do |chunk|
36
+ .. do something with chunk ..
37
+ end
38
+ end
39
+
40
+ See RestClient module docs for more details.
41
+
42
+ == Usage: ActiveResource-Style
43
+
44
+ resource = RestClient::Resource.new 'http://example.com/resource'
45
+ resource.get
46
+
47
+ private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
48
+ private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
49
+
50
+ See RestClient::Resource module docs for details.
51
+
52
+ == Usage: Resource Nesting
53
+
54
+ site = RestClient::Resource.new('http://example.com')
55
+ site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
56
+
57
+ See RestClient::Resource docs for details.
58
+
59
+ == Shell
60
+
61
+ The restclient shell command gives an IRB session with RestClient already loaded:
62
+
63
+ $ restclient
64
+ >> RestClient.get 'http://example.com'
65
+
66
+ Specify a URL argument for get/post/put/delete on that resource:
67
+
68
+ $ restclient http://example.com
69
+ >> put '/resource', 'data'
70
+
71
+ Add a user and password for authenticated resources:
72
+
73
+ $ restclient https://example.com user pass
74
+ >> delete '/private/resource'
75
+
76
+ Create ~/.restclient for named sessions:
77
+
78
+ sinatra:
79
+ url: http://localhost:4567
80
+ rack:
81
+ url: http://localhost:9292
82
+ private_site:
83
+ url: http://example.com
84
+ username: user
85
+ password: pass
86
+
87
+ Then invoke:
88
+
89
+ $ restclient private_site
90
+
91
+ == Meta
92
+
93
+ Written by Adam Wiggins (adam at heroku dot com)
94
+
95
+ Major modifications by Blake Mizerany
96
+
97
+ Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, and Aman Gupta
98
+
99
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
100
+
101
+ http://rest-client.heroku.com
102
+
103
+ http://github.com/adamwiggins/rest-client
104
+