astrotrain 0.3.1
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 +122 -0
- data/VERSION +1 -0
- data/astrotrain.gemspec +129 -0
- data/config/sample.rb +12 -0
- data/lib/astrotrain.rb +53 -0
- data/lib/astrotrain/api.rb +52 -0
- data/lib/astrotrain/logged_mail.rb +46 -0
- data/lib/astrotrain/mapping.rb +157 -0
- data/lib/astrotrain/mapping/http_post.rb +18 -0
- data/lib/astrotrain/mapping/jabber.rb +28 -0
- data/lib/astrotrain/mapping/transport.rb +55 -0
- data/lib/astrotrain/message.rb +330 -0
- data/lib/astrotrain/tmail.rb +58 -0
- data/lib/astrotrain/worker.rb +65 -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.rb +188 -0
- data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +23 -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/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/test/api_test.rb +28 -0
- data/test/fixtures/apple_multipart.txt +100 -0
- data/test/fixtures/bad_content_type.txt +27 -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 +67 -0
- data/test/mapping_test.rb +129 -0
- data/test/message_test.rb +440 -0
- data/test/test_helper.rb +57 -0
- data/test/transport_test.rb +111 -0
- metadata +225 -0
@@ -0,0 +1,46 @@
|
|
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 :sender, String, :index => true, :size => 255, :length => 1..255
|
17
|
+
property :recipient, String, :index => true, :size => 255, :length => 1..255
|
18
|
+
property :subject, String, :index => true, :size => 255, :length => 1..255
|
19
|
+
property :mail_file, String, :size => 255, :length => 1..255
|
20
|
+
property :created_at, DateTime
|
21
|
+
property :delivered_at, DateTime
|
22
|
+
property :error_message, Text
|
23
|
+
|
24
|
+
belongs_to :mapping
|
25
|
+
|
26
|
+
def self.from(message, file = nil)
|
27
|
+
logged = new
|
28
|
+
begin
|
29
|
+
logged.sender = Message.parse_email_addresses(message.sender).first
|
30
|
+
logged.subject = message.subject
|
31
|
+
logged.mail_file = file if file
|
32
|
+
end
|
33
|
+
if !block_given? || yield(logged)
|
34
|
+
begin
|
35
|
+
logged.save
|
36
|
+
if logged.delivered_at && File.exist?(logged.mail_file.to_s)
|
37
|
+
FileUtils.rm_rf logged.mail_file
|
38
|
+
end
|
39
|
+
rescue
|
40
|
+
puts $!.inspect
|
41
|
+
end
|
42
|
+
end
|
43
|
+
logged
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -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, 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
|
+
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 = "#{$!.message}\n#{$!.backtrace.join("\n")}"
|
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,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,330 @@
|
|
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, file = nil)
|
53
|
+
message = parse(raw)
|
54
|
+
Astrotrain.callback(:pre_mapping, message)
|
55
|
+
Mapping.process(message, file)
|
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)
|
62
|
+
raw = IO.read(path)
|
63
|
+
logged_path = path
|
64
|
+
if archive_path
|
65
|
+
daily_archive_path = archive_path / Time.now.year.to_s / Time.now.month.to_s / Time.now.day.to_s
|
66
|
+
FileUtils.mkdir_p(daily_archive_path)
|
67
|
+
logged_path = daily_archive_path / File.basename(path)
|
68
|
+
FileUtils.mv path, logged_path if path != logged_path
|
69
|
+
end
|
70
|
+
receive(raw, logged_path)
|
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 header(key)
|
194
|
+
@headers ||= {}
|
195
|
+
if !@headers.key?(key)
|
196
|
+
@headers[key] = if self.class.skipped_headers.include?(key)
|
197
|
+
nil
|
198
|
+
else
|
199
|
+
header = @mail.header[key]
|
200
|
+
begin
|
201
|
+
header.to_s
|
202
|
+
rescue
|
203
|
+
header.raw_body
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
@headers[key]
|
208
|
+
end
|
209
|
+
|
210
|
+
def headers
|
211
|
+
@headers ||= begin
|
212
|
+
h = {}
|
213
|
+
@mail.header.each do |key, value|
|
214
|
+
header_value = header(key)
|
215
|
+
h[key] = header_value if header_value
|
216
|
+
end
|
217
|
+
h
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class Attachment
|
222
|
+
def initialize(part)
|
223
|
+
@part = part
|
224
|
+
@is_read = false
|
225
|
+
end
|
226
|
+
|
227
|
+
def content_type
|
228
|
+
@part.content_type
|
229
|
+
end
|
230
|
+
|
231
|
+
def filename
|
232
|
+
@filename ||= @part.type_param("name") || @part.disposition_param('filename')
|
233
|
+
end
|
234
|
+
|
235
|
+
# For IO API compatibility when used with Rest-Client
|
236
|
+
def close
|
237
|
+
end
|
238
|
+
|
239
|
+
alias path filename
|
240
|
+
|
241
|
+
def read(value = nil)
|
242
|
+
if read?
|
243
|
+
nil
|
244
|
+
else
|
245
|
+
@is_read = true
|
246
|
+
data
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def read?
|
251
|
+
@is_read == true
|
252
|
+
end
|
253
|
+
|
254
|
+
def data
|
255
|
+
@part.body
|
256
|
+
end
|
257
|
+
|
258
|
+
def attached?
|
259
|
+
!filename.nil?
|
260
|
+
end
|
261
|
+
|
262
|
+
def ==(other)
|
263
|
+
super || (filename == other.filename && content_type == other.content_type)
|
264
|
+
end
|
265
|
+
|
266
|
+
def inspect
|
267
|
+
%(#<Message::Attachment filename=#{filename.inspect} content_type=#{content_type.inspect}>)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
protected
|
272
|
+
def process_message_body
|
273
|
+
if @mail.multipart?
|
274
|
+
@attachments.clear
|
275
|
+
@body, @html = [], []
|
276
|
+
scan_parts(@mail)
|
277
|
+
@body = @body.join("\n")
|
278
|
+
@html = @html.join("\n")
|
279
|
+
else
|
280
|
+
@body = @mail.body
|
281
|
+
@html = ''
|
282
|
+
end
|
283
|
+
if !@mail.charset
|
284
|
+
@body = convert_to_utf8(@body)
|
285
|
+
@html = convert_to_utf8(@html)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def scan_parts(message)
|
290
|
+
message.parts.each do |part|
|
291
|
+
if part.multipart?
|
292
|
+
scan_parts(part)
|
293
|
+
else
|
294
|
+
case part.content_type
|
295
|
+
when 'text/plain'
|
296
|
+
@body << part.body
|
297
|
+
when 'text/html'
|
298
|
+
@html << part.body
|
299
|
+
else
|
300
|
+
att = Attachment.new(part)
|
301
|
+
@attachments << att if att.attached?
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def parse_email_headers(values, collection)
|
308
|
+
values.each do |value|
|
309
|
+
if !value.blank?
|
310
|
+
collection.push *self.class.parse_email_addresses(value)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Attempts to run iconv conversions in common charsets to UTF-8. Needed for
|
316
|
+
# those crappy emails that don't properly specify a charset in the headers.
|
317
|
+
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
|
318
|
+
ISO-8859-15 GB2312)
|
319
|
+
def convert_to_utf8(s)
|
320
|
+
ICONV_CONVERSIONS.each do |from|
|
321
|
+
begin
|
322
|
+
return Iconv.iconv(ICONV_CONVERSIONS[0], from, s).to_s
|
323
|
+
rescue Iconv::IllegalSequence
|
324
|
+
ensure
|
325
|
+
s
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|