entp-astrotrain 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/LICENSE +20 -0
- data/README +47 -0
- data/Rakefile +145 -0
- data/VERSION +1 -0
- data/astrotrain.gemspec +96 -0
- data/config/sample.rb +12 -0
- data/lib/astrotrain/api.rb +53 -0
- data/lib/astrotrain/logged_mail.rb +41 -0
- data/lib/astrotrain/mapping/http_post.rb +18 -0
- data/lib/astrotrain/mapping/jabber.rb +23 -0
- data/lib/astrotrain/mapping/transport.rb +55 -0
- data/lib/astrotrain/mapping.rb +157 -0
- data/lib/astrotrain/message.rb +313 -0
- data/lib/astrotrain/tmail.rb +48 -0
- data/lib/astrotrain.rb +55 -0
- data/lib/vendor/rest-client/README.rdoc +104 -0
- data/lib/vendor/rest-client/Rakefile +84 -0
- data/lib/vendor/rest-client/bin/restclient +65 -0
- data/lib/vendor/rest-client/foo.diff +66 -0
- data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +21 -0
- data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
- data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
- data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
- data/lib/vendor/rest-client/lib/rest_client.rb +189 -0
- data/lib/vendor/rest-client/rest-client.gemspec +18 -0
- data/lib/vendor/rest-client/spec/base.rb +5 -0
- data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
- data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
- data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
- data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
- data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
- data/tasks/doc.thor +149 -0
- data/tasks/merb.thor +2020 -0
- data/test/api_test.rb +28 -0
- data/test/fixtures/apple_multipart.txt +100 -0
- data/test/fixtures/basic.txt +14 -0
- data/test/fixtures/custom.txt +15 -0
- data/test/fixtures/fwd.txt +0 -0
- data/test/fixtures/gb2312_encoding.txt +16 -0
- data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
- data/test/fixtures/html.txt +16 -0
- data/test/fixtures/iso-8859-1.txt +13 -0
- data/test/fixtures/mapped.txt +13 -0
- data/test/fixtures/multipart.txt +213 -0
- data/test/fixtures/multipart2.txt +213 -0
- data/test/fixtures/multiple.txt +13 -0
- data/test/fixtures/multiple_delivered_to.txt +14 -0
- data/test/fixtures/multiple_with_body_recipients.txt +15 -0
- data/test/fixtures/reply.txt +16 -0
- data/test/fixtures/utf-8.txt +13 -0
- data/test/logged_mail_test.rb +63 -0
- data/test/mapping_test.rb +129 -0
- data/test/message_test.rb +424 -0
- data/test/test_helper.rb +54 -0
- data/test/transport_test.rb +111 -0
- 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
|
+
|