kenhirakawa-astrotrain 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +26 -0
  2. data/LICENSE +20 -0
  3. data/README +47 -0
  4. data/Rakefile +124 -0
  5. data/VERSION +1 -0
  6. data/astrotrain.gemspec +141 -0
  7. data/config/sample.rb +12 -0
  8. data/lib/astrotrain.rb +56 -0
  9. data/lib/astrotrain/api.rb +52 -0
  10. data/lib/astrotrain/logged_mail.rb +48 -0
  11. data/lib/astrotrain/mapping.rb +162 -0
  12. data/lib/astrotrain/mapping/http_post.rb +18 -0
  13. data/lib/astrotrain/mapping/jabber.rb +28 -0
  14. data/lib/astrotrain/mapping/transport.rb +55 -0
  15. data/lib/astrotrain/message.rb +342 -0
  16. data/lib/astrotrain/tmail.rb +58 -0
  17. data/lib/astrotrain/worker.rb +65 -0
  18. data/lib/vendor/rest-client/README.rdoc +104 -0
  19. data/lib/vendor/rest-client/Rakefile +84 -0
  20. data/lib/vendor/rest-client/bin/restclient +65 -0
  21. data/lib/vendor/rest-client/foo.diff +66 -0
  22. data/lib/vendor/rest-client/lib/rest_client.rb +188 -0
  23. data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +23 -0
  24. data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
  25. data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
  26. data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
  27. data/lib/vendor/rest-client/rest-client.gemspec +18 -0
  28. data/lib/vendor/rest-client/spec/base.rb +5 -0
  29. data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
  30. data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
  31. data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
  32. data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
  33. data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
  34. data/test/api_test.rb +32 -0
  35. data/test/fixtures/apple_multipart.txt +100 -0
  36. data/test/fixtures/bad_content_type.txt +27 -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 +10 -0
  43. data/test/fixtures/html_multipart.txt +16 -0
  44. data/test/fixtures/iso-8859-1.txt +13 -0
  45. data/test/fixtures/mapped.txt +13 -0
  46. data/test/fixtures/multipart.txt +213 -0
  47. data/test/fixtures/multipart2.txt +213 -0
  48. data/test/fixtures/multiple.txt +13 -0
  49. data/test/fixtures/multiple_delivered_to.txt +14 -0
  50. data/test/fixtures/multiple_with_body_recipients.txt +15 -0
  51. data/test/fixtures/reply.txt +16 -0
  52. data/test/fixtures/undisclosed.txt +14 -0
  53. data/test/fixtures/utf-8.txt +13 -0
  54. data/test/logged_mail_test.rb +67 -0
  55. data/test/mapping_test.rb +129 -0
  56. data/test/message_test.rb +492 -0
  57. data/test/test_helper.rb +56 -0
  58. data/test/transport_test.rb +113 -0
  59. metadata +330 -0
@@ -0,0 +1,48 @@
1
+ module Astrotrain
2
+ # Logs details of each incoming message.
3
+ class LoggedMail
4
+ include DataMapper::Resource
5
+
6
+ class << self
7
+ attr_accessor :log_path, :log_processed
8
+ end
9
+
10
+ # Enabling this will save records for every processed email, not just the errored emails.
11
+ self.log_processed = false
12
+ self.log_path = File.join(Astrotrain.root, 'messages')
13
+
14
+ property :id, Serial
15
+ property :mapping_id, Integer, :index => true
16
+ property :message_id, String, :index => true, :size => 255, :length => 1..255
17
+ property :sender, String, :index => true, :size => 255, :length => 1..255
18
+ property :recipient, String, :index => true, :size => 255, :length => 1..255
19
+ property :subject, String, :index => true, :size => 255, :length => 1..255
20
+ property :mail_file, String, :size => 255, :length => 1..255
21
+ property :created_at, DateTime
22
+ property :delivered_at, DateTime
23
+ property :error_message, Text
24
+
25
+ belongs_to :mapping
26
+
27
+ def self.from(message, file = nil)
28
+ logged = new
29
+ begin
30
+ logged.message_id = message.message_id
31
+ logged.sender = Message.parse_email_addresses(message.sender).first
32
+ logged.subject = message.subject
33
+ logged.mail_file = file if file
34
+ end
35
+ if !block_given? || yield(logged)
36
+ begin
37
+ logged.save
38
+ if logged.delivered_at && File.exist?(logged.mail_file.to_s)
39
+ FileUtils.rm_rf logged.mail_file
40
+ end
41
+ rescue
42
+ puts $!.inspect
43
+ end
44
+ end
45
+ logged
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,162 @@
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, file = nil)
42
+ LoggedMail.from(message, file) do |logged|
43
+ save_logged = begin
44
+ mapping, recipient = match(message.recipients)
45
+ if mapping
46
+ logged.recipient = recipient
47
+ logged.mapping = mapping
48
+ begin
49
+ mapping.process(message, recipient)
50
+ rescue Astrotrain::ProcessingCancelled
51
+ logged.error_message = "Cancelled."
52
+ end
53
+ logged.delivered_at = Time.now.utc
54
+ end
55
+ LoggedMail.log_processed # save successfully processed messages?
56
+ rescue
57
+ logged.error_message = "#{$!.message}\n#{$!.backtrace.join("\n")}"
58
+ end
59
+ Astrotrain.callback(:post_processing, message, mapping, logged)
60
+ save_logged
61
+ end
62
+ end
63
+
64
+ # Processes a given message and recipient against the mapping's transport.
65
+ def process(message, recipient)
66
+ Astrotrain.callback(:pre_processing, message, self)
67
+ Transport.process(message, self, recipient)
68
+
69
+ end
70
+
71
+ # returns true if the email matches this mapping. Wildcards in the name are allowed.
72
+ # A mapping with foo*@bar.com will match foo@bar.com and food@bar.com, but not foo@baz.com.
73
+ def match?(name, domain)
74
+ email_domain == domain && name =~ email_user_regex
75
+ end
76
+
77
+ def destination_uses_url?
78
+ transport == 'http_post'
79
+ end
80
+
81
+ def destination_uses_email?
82
+ transport == 'jabber'
83
+ end
84
+
85
+ def full_email
86
+ "#{email_user}@#{email_domain}"
87
+ end
88
+
89
+ # Looks for the mapping's separator in the message body and pulls only the content
90
+ # above it. Assuming a separator of '===='...
91
+ #
92
+ # This will be kept
93
+ #
94
+ # On Thu, Sep 3, 2009 at 12:34 AM... (this will be removed)
95
+ # ====
96
+ #
97
+ # > Everything here will be removed.
98
+ #
99
+ def find_reply_from(body)
100
+ return if separator.blank?
101
+ return '' if body.blank?
102
+ lines = body.split("\n")
103
+ delim_line = found_empty = nil
104
+
105
+ (lines.size - 1).downto(0) do |i|
106
+ line = lines[i]
107
+ if !delim_line && line.include?(separator)
108
+ delim_line = i
109
+ elsif delim_line && !found_empty
110
+ delim_line = i
111
+ found_empty = line.strip.blank?
112
+ elsif delim_line && found_empty
113
+ if date_reply_line?(line) || line.strip.blank?
114
+ delim_line = i
115
+ else
116
+ break
117
+ end
118
+ end
119
+ end
120
+
121
+ if delim_line
122
+ body = if delim_line.zero?
123
+ []
124
+ elsif lines.size >= delim_line
125
+ lines[0..delim_line-1]
126
+ else
127
+ lines
128
+ end.join("\n")
129
+ elsif body.frozen?
130
+ body = body.dup
131
+ end
132
+ body.strip!
133
+ body
134
+ end
135
+
136
+ protected
137
+ def self.match_by_address(name, domain)
138
+ first(:email_user => name, :email_domain => domain)
139
+ end
140
+
141
+ def self.match_by_wildcard(name, domain)
142
+ wildcards = all(:email_domain => domain, :email_user.like => "%*")
143
+ wildcards.sort! { |x, y| y.email_user.size <=> x.email_user.size }
144
+ wildcards.detect { |w| w.match?(name, domain) }
145
+ end
146
+
147
+ DATE_LANGUATE_REGEXES = [/^on\b.*wrote\b?:$/i, /^am\b.*schrieb [\w\d\s]+:$/i, /^le\b.*a écrit\b?:$/i]
148
+ def date_reply_line?(line)
149
+ DATE_LANGUATE_REGEXES.any? { |re| line =~ re }
150
+ end
151
+
152
+ def email_user_regex
153
+ @email_user_regex ||= begin
154
+ if email_user['*']
155
+ /^#{email_user.sub /\*/, '(.*)'}$/
156
+ else
157
+ /^#{email_user}$/
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,18 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ class HttpPost < Transport
4
+ def process
5
+ return unless Transport.processing
6
+ RestClient.post @mapping.destination, fields.merge(:emails => fields[:emails].join(","))
7
+ end
8
+
9
+ def fields
10
+ super
11
+ @message.attachments.each_with_index do |att, index|
12
+ @fields[:"attachments_#{index}"] = att
13
+ end
14
+ @fields
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'xmpp4r-simple'
3
+ module Astrotrain
4
+ class Mapping
5
+ # This is experimental. No attachments are supported.
6
+ class Jabber < Transport
7
+ class << self
8
+ attr_accessor :login, :password
9
+ end
10
+
11
+ def process
12
+ return unless Transport.processing
13
+ connection.deliver(@mapping.destination, content)
14
+ end
15
+
16
+ def connection
17
+ @connection ||= ::Jabber::Simple.new(self.class.login, self.class.password)
18
+ end
19
+
20
+ def content
21
+ @content ||= "From: %s\nTo: %s\nSubject: %s\nEmails: %s\n%s" % [fields[:from], fields[:to], fields[:subject], fields[:emails] * ", ", fields[:body]]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ rescue LoadError
27
+ puts "Install xmpp4r-simple for Jabber support."
28
+ end
@@ -0,0 +1,55 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ class Transport
4
+ class << self
5
+ attr_accessor :processing
6
+ end
7
+
8
+ # Enable this turn on processing.
9
+ self.processing = false
10
+
11
+ attr_reader :message, :mapping
12
+
13
+ # process a given message against the mapping. The mapping transport is checked,
14
+ # and the appropirate transport class handles the request.
15
+ def self.process(message, mapping, recipient)
16
+ case mapping.transport
17
+ when 'http_post' then HttpPost.process(message, mapping, recipient)
18
+ when 'jabber' then Jabber.process(message, mapping, recipient)
19
+ end
20
+ end
21
+
22
+ def initialize(message, mapping, recipient)
23
+ message.body = mapping.find_reply_from(message.body)
24
+ @message = message
25
+ @mapping = mapping
26
+ @recipient = recipient
27
+ end
28
+
29
+ def process
30
+ raise UnimplementedError
31
+ end
32
+
33
+ def fields
34
+ @fields ||= begin
35
+ all_emails = @message.recipients - [@recipient]
36
+ f = {:subject => @message.subject, :to => @recipient, :from => @message.sender, :body => @message.body, :emails => all_emails, :html => @message.html}
37
+ @message.headers.each do |key, value|
38
+ f["headers[#{key}]"] = value
39
+ end
40
+ f
41
+ end
42
+ end
43
+
44
+ # defines custom #process class methods that instantiate the class and calls a #process instance method
45
+ def self.inherited(child)
46
+ super
47
+ class << child
48
+ def process(message, mapping, recipient)
49
+ new(message, mapping, recipient).process
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,342 @@
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
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, file = nil)
53
+ message = parse(raw)
54
+ Astrotrain.callback(:pre_mapping, message)
55
+ Mapping.process(message, file)
56
+ message
57
+ rescue Astrotrain::ProcessingCancelled
58
+ end
59
+
60
+ # Processes the given file. It parses it by reading the contents, and optionally
61
+ # archives or removes the original file.
62
+ def self.receive_file(path)
63
+ raw = IO.read(path)
64
+ logged_path = path
65
+ if archive_path
66
+ daily_archive_path = archive_path / Time.now.year.to_s / Time.now.month.to_s / Time.now.day.to_s
67
+ FileUtils.mkdir_p(daily_archive_path)
68
+ logged_path = daily_archive_path / File.basename(path)
69
+ FileUtils.mv path, logged_path if path != logged_path
70
+ end
71
+ receive(raw, logged_path)
72
+ end
73
+
74
+ # Parses the raw email headers into a Astrotrain::Message instance.
75
+ def self.parse(raw)
76
+ new Mail.parse(raw)
77
+ end
78
+
79
+ def self.parse_email_addresses(value)
80
+ emails = value.split(",")
81
+ collection = []
82
+ emails.each do |addr|
83
+ addr.strip!
84
+ next if addr.blank?
85
+ header = parse_email_address(addr.to_s)
86
+ collection << unescape(header[:email]) if !header[:email].blank?
87
+ end
88
+ collection
89
+ end
90
+
91
+ def self.parse_email_address(email)
92
+ return {} if email.blank?
93
+ begin
94
+ header = TMail::Address.parse(email)
95
+ parsed = {:name => header.name}
96
+ if header.is_a?(TMail::AddressGroup)
97
+ header = header[0]
98
+ end
99
+ if !header.blank?
100
+ parsed[:email] = header.address
101
+ end
102
+ parsed
103
+ rescue SyntaxError, TMail::SyntaxError
104
+ email = email.scan(/\<([^\>]+)\>/)[0]
105
+ if email.blank?
106
+ return {:name => nil, :email => nil}
107
+ else
108
+ email = email[0]
109
+ retry
110
+ end
111
+ end
112
+ end
113
+
114
+ # Stolen from Rack/Camping, remove the "+" => " " translation
115
+ def self.unescape(s)
116
+ s.gsub!(/((?:%[0-9a-fA-F]{2})+)/n){
117
+ [$1.delete('%')].pack('H*')
118
+ }
119
+ s
120
+ end
121
+
122
+ def initialize(mail)
123
+ @mail = mail
124
+ @mapping = nil
125
+ @attachments = []
126
+ @recipients = {}
127
+ end
128
+
129
+ # Gets the recipients of an email using the To/Delivered-To/X-Original-To headers.
130
+ # It's not always straightforward which email we want when dealing with filters
131
+ # and forward rules.
132
+ def recipients(order = nil)
133
+ if !@recipients.key?(order)
134
+ order = self.class.recipient_header_order if order.blank?
135
+ recipients = []
136
+
137
+ order.each do |key|
138
+ parse_email_headers(send("recipients_from_#{key}"), recipients)
139
+ end
140
+ parse_email_headers recipients_from_body, recipients
141
+
142
+ recipients.flatten!
143
+ recipients.uniq!
144
+ @recipients[order] = recipients
145
+ else
146
+ @recipients[order]
147
+ end
148
+ end
149
+
150
+ def recipients_from_to
151
+ @recipient_from_to ||= [@mail['to'].to_s]
152
+ end
153
+
154
+ def recipients_from_delivered_to
155
+ @recipient_from_delivered_to ||= begin
156
+ delivered = @mail['Delivered-To']
157
+ if delivered.respond_to?(:first)
158
+ delivered.map! { |a| a.to_s }
159
+ else
160
+ [delivered.to_s]
161
+ end
162
+ end
163
+ end
164
+
165
+ def recipients_from_original_to
166
+ @recipient_from_original_to ||= [@mail['X-Original-To'].to_s]
167
+ end
168
+
169
+ def recipients_from_body
170
+ @recipients_from_body ||= body.scan(/<[\w\.\_\%\+\-]+@[\w\-\_\.]+>/)
171
+ end
172
+
173
+ def sender
174
+ @sender ||= TMail::Unquoter.unquote_and_convert_to(@mail['from'].to_s, "utf-8")
175
+ end
176
+
177
+ def subject
178
+ @mail.subject
179
+ rescue Iconv::InvalidCharacter
180
+ @mail.quoted_subject
181
+ end
182
+
183
+ def message_id
184
+ @message_id ||= header('message-id').to_s.gsub(/^<|>$/, '')
185
+ end
186
+
187
+ def body
188
+ @body ||= process_message_body(:body)
189
+ end
190
+
191
+ def html
192
+ @html ||= process_message_body(:html)
193
+ end
194
+
195
+ def attachments
196
+ @attachments ||= process_message_body(:attachments)
197
+ end
198
+
199
+ def raw
200
+ @mail.port.to_s
201
+ end
202
+
203
+ def header(key)
204
+ headers[key]
205
+ end
206
+
207
+ def headers
208
+ @headers ||= begin
209
+ h = {}
210
+ @mail.header.each do |key, value|
211
+ next if self.class.skipped_headers.include?(key)
212
+ h[key] = read_header(key)
213
+ end
214
+ h
215
+ end
216
+ end
217
+
218
+ class Attachment
219
+ def initialize(part)
220
+ @part = part
221
+ @is_read = false
222
+ end
223
+
224
+ def content_type
225
+ @part.content_type
226
+ end
227
+
228
+ def filename
229
+ @filename ||= @part.type_param("name") || @part.disposition_param('filename')
230
+ end
231
+
232
+ # For IO API compatibility when used with Rest-Client
233
+ def close
234
+ end
235
+
236
+ alias path filename
237
+
238
+ def read(value = nil)
239
+ if read?
240
+ nil
241
+ else
242
+ @is_read = true
243
+ data
244
+ end
245
+ end
246
+
247
+ def read?
248
+ @is_read == true
249
+ end
250
+
251
+ def data
252
+ @part.body
253
+ end
254
+
255
+ def attached?
256
+ !filename.nil?
257
+ end
258
+
259
+ def ==(other)
260
+ super || (filename == other.filename && content_type == other.content_type)
261
+ end
262
+
263
+ def inspect
264
+ %(#<Message::Attachment filename=#{filename.inspect} content_type=#{content_type.inspect}>)
265
+ end
266
+ end
267
+
268
+ protected
269
+ def read_header(key)
270
+ header = @mail.header[key]
271
+ begin
272
+ header.to_s
273
+ rescue
274
+ header.raw_body
275
+ end
276
+ end
277
+
278
+ def process_message_body(var = nil)
279
+ if @mail.multipart?
280
+ @attachments.clear
281
+ @body, @html = [], []
282
+ scan_parts(@mail)
283
+ @body = @body.join("\n")
284
+ @html = @html.join("\n")
285
+ else
286
+ if @mail.content_type == 'text/html'
287
+ @html = @mail.body
288
+ @body = ''
289
+ else
290
+ @body = @mail.body
291
+ @html = ''
292
+ end
293
+ end
294
+ if !@mail.charset
295
+ @body = convert_to_utf8(@body)
296
+ @html = convert_to_utf8(@html)
297
+ end
298
+ instance_variable_get "@#{var}" if var
299
+ end
300
+
301
+ def scan_parts(message)
302
+ message.parts.each do |part|
303
+ if part.multipart?
304
+ scan_parts(part)
305
+ else
306
+ case part.content_type
307
+ when 'text/plain'
308
+ @body << part.body
309
+ when 'text/html'
310
+ @html << part.body
311
+ else
312
+ att = Attachment.new(part)
313
+ @attachments << att if att.attached?
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def parse_email_headers(values, collection)
320
+ values.each do |value|
321
+ if !value.blank?
322
+ collection.push *self.class.parse_email_addresses(value)
323
+ end
324
+ end
325
+ end
326
+
327
+ # Attempts to run iconv conversions in common charsets to UTF-8. Needed for
328
+ # those crappy emails that don't properly specify a charset in the headers.
329
+ 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
330
+ ISO-8859-15 GB2312)
331
+ def convert_to_utf8(s)
332
+ ICONV_CONVERSIONS.each do |from|
333
+ begin
334
+ return Iconv.iconv(ICONV_CONVERSIONS[0], from, s).to_s
335
+ rescue Iconv::IllegalSequence
336
+ ensure
337
+ s
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end