ruby-gmail 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +1 -7
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/gmail.rb +113 -69
- data/lib/gmail/message.rb +21 -23
- data/lib/smtp_tls.rb +2 -2
- data/ruby-gmail.gemspec +5 -10
- metadata +8 -13
- data/lib/ietf/rfc2045.rb +0 -29
- data/lib/ietf/rfc822.rb +0 -22
- data/lib/mime/entity.rb +0 -202
- data/lib/mime/entity_tmail.rb +0 -219
- data/lib/mime/message.rb +0 -82
data/README.markdown
CHANGED
@@ -1,9 +1,3 @@
|
|
1
|
-
# This fork
|
2
|
-
|
3
|
-
contains a fix to a bug in the initializer, which was discussed [here](http://github.com/dcparker/ruby-gmail/issues#issue/7/comment/141607)
|
4
|
-
|
5
|
-
* * * *
|
6
|
-
|
7
1
|
# ruby-gmail
|
8
2
|
|
9
3
|
* Homepage: [http://dcparker.github.com/ruby-gmail/](http://dcparker.github.com/ruby-gmail/)
|
@@ -89,7 +83,7 @@ A Rubyesque interface to Gmail, with all the tools you'll need. Search, read and
|
|
89
83
|
end
|
90
84
|
|
91
85
|
# delete emails from X...
|
92
|
-
gmail.inbox.emails(:from => "x-
|
86
|
+
gmail.inbox.emails(:from => "x-fiancé@gmail.com").each do |email|
|
93
87
|
email.delete!
|
94
88
|
end
|
95
89
|
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ begin
|
|
13
13
|
gem.authors = ["BehindLogic"]
|
14
14
|
gem.post_install_message = "\n\033[34mIf ruby-gmail saves you TWO hours of work, want to compensate me for, like, a half-hour?\nSupport me in making new and better gems:\033[0m \033[31;4mhttp://pledgie.com/campaigns/7087\033[0m\n\n"
|
15
15
|
gem.add_dependency('shared-mime-info', '>= 0')
|
16
|
-
gem.add_dependency('
|
16
|
+
gem.add_dependency('mail', '>= 2.2.1')
|
17
17
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
18
|
end
|
19
19
|
Jeweler::GemcutterTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/gmail.rb
CHANGED
@@ -1,116 +1,145 @@
|
|
1
1
|
require 'net/imap'
|
2
|
-
require 'net/smtp'
|
3
|
-
require 'smtp_tls'
|
4
2
|
|
5
3
|
class Gmail
|
6
4
|
VERSION = '0.0.9'
|
7
5
|
|
8
6
|
class NoLabel < RuntimeError; end
|
9
7
|
|
8
|
+
##################################
|
9
|
+
# Gmail.new(username, password)
|
10
|
+
##################################
|
10
11
|
def initialize(username, password)
|
11
12
|
# This is to hide the username and password, not like it REALLY needs hiding, but ... you know.
|
12
13
|
# Could be helpful when demoing the gem in irb, these bits won't show up that way.
|
13
|
-
|
14
|
+
class << self
|
14
15
|
class << self
|
15
16
|
attr_accessor :username, :password
|
16
17
|
end
|
17
|
-
self
|
18
18
|
end
|
19
19
|
meta.username = username =~ /@/ ? username : username + '@gmail.com'
|
20
20
|
meta.password = password
|
21
21
|
@imap = Net::IMAP.new('imap.gmail.com',993,true,nil,false)
|
22
|
-
@imap.login(username, password)
|
23
22
|
if block_given?
|
23
|
+
login # This is here intentionally. Normally, we get auto logged-in when first needed.
|
24
24
|
yield self
|
25
25
|
logout
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
###########################
|
30
|
+
# READING EMAILS
|
31
|
+
#
|
32
|
+
# gmail.inbox
|
33
|
+
# gmail.label('News')
|
34
|
+
#
|
35
|
+
###########################
|
33
36
|
|
34
|
-
# Accessors for Gmail things
|
35
37
|
def inbox
|
36
|
-
|
38
|
+
in_label('inbox')
|
37
39
|
end
|
38
|
-
|
39
|
-
def imap
|
40
|
-
if @imap.disconnected?
|
41
|
-
meta = class << self; self end
|
42
|
-
@imap.login(meta.username, meta.password)
|
43
|
-
at_exit { logout } # Set up auto-logout for later.
|
44
|
-
end
|
45
|
-
@imap
|
46
|
-
end
|
47
|
-
# Log out of gmail
|
48
|
-
def logout
|
49
|
-
@imap.logout unless @imap.disconnected?
|
50
|
-
end
|
51
|
-
|
40
|
+
|
52
41
|
def create_label(name)
|
53
42
|
imap.create(name)
|
54
43
|
end
|
55
44
|
|
56
45
|
# List the available labels
|
57
46
|
def labels
|
58
|
-
(
|
47
|
+
(imap.list("", "%") + imap.list("[Gmail]/", "%")).inject([]) { |labels,label|
|
59
48
|
label[:name].each_line { |l| labels << l }; labels }
|
60
49
|
end
|
61
50
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
unless @selected == mailbox.name
|
66
|
-
imap.select(mailbox.name)
|
67
|
-
@selected = mailbox.name
|
68
|
-
end
|
69
|
-
value = block.arity == 1 ? block.call(mailbox) : block.call
|
70
|
-
mailbox_stack.pop
|
71
|
-
# Select previously selected mailbox if there is one
|
72
|
-
if mailbox_stack.last
|
73
|
-
imap.select(mailbox_stack.last.name)
|
74
|
-
@selected = mailbox.name
|
75
|
-
end
|
76
|
-
return value
|
51
|
+
# gmail.label(name)
|
52
|
+
def label(name)
|
53
|
+
mailboxes[name] ||= Mailbox.new(self, mailbox)
|
77
54
|
end
|
78
|
-
alias :
|
55
|
+
alias :mailbox :label
|
79
56
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
57
|
+
###########################
|
58
|
+
# MAKING EMAILS
|
59
|
+
#
|
60
|
+
# gmail.generate_message do
|
61
|
+
# ...inside Mail context...
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# gmail.deliver do ... end
|
65
|
+
#
|
66
|
+
# mail = Mail.new...
|
67
|
+
# gmail.deliver!(mail)
|
68
|
+
###########################
|
69
|
+
def generate_message(&block)
|
70
|
+
require 'net/smtp'
|
71
|
+
require 'smtp_tls'
|
72
|
+
require 'mail'
|
73
|
+
mail = Mail.new(&block)
|
74
|
+
mail.delivery_method(*smtp_settings)
|
75
|
+
mail
|
94
76
|
end
|
95
|
-
|
96
|
-
def
|
97
|
-
|
77
|
+
|
78
|
+
def deliver(mail=nil, &block)
|
79
|
+
require 'net/smtp'
|
80
|
+
require 'smtp_tls'
|
81
|
+
require 'mail'
|
82
|
+
mail = Mail.new(&block) if block_given?
|
83
|
+
mail.delivery_method(*smtp_settings)
|
84
|
+
mail.from = meta.username unless mail.from
|
85
|
+
mail.deliver!
|
98
86
|
end
|
99
87
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
88
|
+
###########################
|
89
|
+
# LOGIN
|
90
|
+
###########################
|
91
|
+
def login
|
92
|
+
res = @imap.login(meta.username, meta.password)
|
93
|
+
@logged_in = true if res.name == 'OK'
|
94
|
+
end
|
95
|
+
def logged_in?
|
96
|
+
!!@logged_in
|
97
|
+
end
|
98
|
+
# Log out of gmail
|
99
|
+
def logout
|
100
|
+
if logged_in?
|
101
|
+
res = @imap.logout
|
102
|
+
@logged_in = false if res.name == 'OK'
|
107
103
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
104
|
+
end
|
105
|
+
|
106
|
+
def in_mailbox(mailbox, &block)
|
107
|
+
if block_given?
|
108
|
+
mailbox_stack << mailbox
|
109
|
+
unless @selected == mailbox.name
|
110
|
+
imap.select(mailbox.name)
|
111
|
+
@selected = mailbox.name
|
112
|
+
end
|
113
|
+
value = block.arity == 1 ? block.call(mailbox) : block.call
|
114
|
+
mailbox_stack.pop
|
115
|
+
# Select previously selected mailbox if there is one
|
116
|
+
if mailbox_stack.last
|
117
|
+
imap.select(mailbox_stack.last.name)
|
118
|
+
@selected = mailbox.name
|
119
|
+
end
|
120
|
+
return value
|
121
|
+
else
|
122
|
+
mailboxes[name] ||= Mailbox.new(self, mailbox)
|
111
123
|
end
|
112
124
|
end
|
125
|
+
alias :in_label :in_mailbox
|
126
|
+
|
127
|
+
###########################
|
128
|
+
# Other...
|
129
|
+
###########################
|
130
|
+
def inspect
|
131
|
+
"#<Gmail:#{'0x%x' % (object_id << 1)} (#{meta.username}) #{'dis' if !logged_in?}connected>"
|
132
|
+
end
|
113
133
|
|
134
|
+
# Accessor for @imap, but ensures that it's logged in first.
|
135
|
+
def imap
|
136
|
+
unless logged_in?
|
137
|
+
login
|
138
|
+
at_exit { logout } # Set up auto-logout for later.
|
139
|
+
end
|
140
|
+
@imap
|
141
|
+
end
|
142
|
+
|
114
143
|
private
|
115
144
|
def mailboxes
|
116
145
|
@mailboxes ||= {}
|
@@ -118,6 +147,21 @@ class Gmail
|
|
118
147
|
def mailbox_stack
|
119
148
|
@mailbox_stack ||= []
|
120
149
|
end
|
150
|
+
def meta
|
151
|
+
class << self; self end
|
152
|
+
end
|
153
|
+
def domain
|
154
|
+
meta.username.split('@')[0]
|
155
|
+
end
|
156
|
+
def smtp_settings
|
157
|
+
[:smtp, {:address => "smtp.gmail.com",
|
158
|
+
:port => 587,
|
159
|
+
:domain => domain,
|
160
|
+
:user_name => meta.username,
|
161
|
+
:password => meta.password,
|
162
|
+
:authentication => 'plain',
|
163
|
+
:enable_starttls_auto => true}]
|
164
|
+
end
|
121
165
|
end
|
122
166
|
|
123
167
|
require 'gmail/mailbox'
|
data/lib/gmail/message.rb
CHANGED
@@ -16,37 +16,17 @@ class Gmail
|
|
16
16
|
@uid ||= @gmail.imap.uid_search(['HEADER', 'Message-ID', message_id])[0]
|
17
17
|
end
|
18
18
|
|
19
|
-
def message_id
|
20
|
-
@message_id || begin
|
21
|
-
@gmail.in_mailbox(@mailbox) do
|
22
|
-
@message_id = @gmail.imap.uid_fetch(@uid, ['ENVELOPE'])[0].attr['ENVELOPE'].message_id
|
23
|
-
end
|
24
|
-
end
|
25
|
-
@message_id
|
26
|
-
end
|
27
|
-
|
28
|
-
def body
|
29
|
-
@body ||= @gmail.in_mailbox(@mailbox) do
|
30
|
-
@gmail.imap.uid_fetch(uid, "RFC822")[0].attr["RFC822"]
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Parsed MIME message object
|
35
|
-
def message
|
36
|
-
@message ||= MIME::Message.new(body)
|
37
|
-
end
|
38
|
-
|
39
19
|
# IMAP Operations
|
40
20
|
def flag(flg)
|
41
21
|
@gmail.in_mailbox(@mailbox) do
|
42
22
|
@gmail.imap.uid_store(uid, "+FLAGS", [flg])
|
43
|
-
end
|
23
|
+
end ? true : false
|
44
24
|
end
|
45
25
|
|
46
26
|
def unflag(flg)
|
47
27
|
@gmail.in_mailbox(@mailbox) do
|
48
28
|
@gmail.imap.uid_store(uid, "-FLAGS", [flg])
|
49
|
-
end
|
29
|
+
end ? true : false
|
50
30
|
end
|
51
31
|
|
52
32
|
# Gmail Operations
|
@@ -60,7 +40,7 @@ class Gmail
|
|
60
40
|
flag(:Deleted)
|
61
41
|
when :spam
|
62
42
|
move_to('[Gmail]/Spam')
|
63
|
-
end
|
43
|
+
end ? true : false
|
64
44
|
end
|
65
45
|
|
66
46
|
def delete!
|
@@ -102,5 +82,23 @@ class Gmail
|
|
102
82
|
def archive!
|
103
83
|
move_to('[Gmail]/All Mail')
|
104
84
|
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Parsed MIME message object
|
89
|
+
def message
|
90
|
+
require 'mail'
|
91
|
+
_body = @gmail.in_mailbox(@mailbox) { @gmail.imap.uid_fetch(uid, "RFC822")[0].attr["RFC822"] }
|
92
|
+
@message ||= Mail.new(_body)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Delegate all other methods to the Mail message
|
96
|
+
def method_missing(*args, &block)
|
97
|
+
if block_given?
|
98
|
+
message.send(*args, &block)
|
99
|
+
else
|
100
|
+
message.send(*args)
|
101
|
+
end
|
102
|
+
end
|
105
103
|
end
|
106
104
|
end
|
data/lib/smtp_tls.rb
CHANGED
@@ -11,8 +11,8 @@ Net::SMTP.class_eval do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def start( helo = 'localhost.localdomain',
|
14
|
-
user = nil, secret = nil, authtype = nil
|
15
|
-
start_method =
|
14
|
+
user = nil, secret = nil, authtype = nil ) # :yield: smtp
|
15
|
+
start_method = starttls_auto? ? :do_tls_start : :do_start
|
16
16
|
if block_given?
|
17
17
|
begin
|
18
18
|
send(start_method, helo, user, secret, authtype)
|
data/ruby-gmail.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{ruby-gmail}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["BehindLogic"]
|
12
|
-
s.date = %q{2010-05-
|
12
|
+
s.date = %q{2010-05-14}
|
13
13
|
s.description = %q{A Rubyesque interface to Gmail, with all the tools you'll need. Search, read and send multipart emails; archive, mark as read/unread, delete emails; and manage labels.}
|
14
14
|
s.email = %q{gems@behindlogic.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -26,11 +26,6 @@ Gem::Specification.new do |s|
|
|
26
26
|
"lib/gmail.rb",
|
27
27
|
"lib/gmail/mailbox.rb",
|
28
28
|
"lib/gmail/message.rb",
|
29
|
-
"lib/ietf/rfc2045.rb",
|
30
|
-
"lib/ietf/rfc822.rb",
|
31
|
-
"lib/mime/entity.rb",
|
32
|
-
"lib/mime/entity_tmail.rb",
|
33
|
-
"lib/mime/message.rb",
|
34
29
|
"lib/smtp_tls.rb",
|
35
30
|
"ruby-gmail.gemspec",
|
36
31
|
"test/test_gmail.rb",
|
@@ -57,14 +52,14 @@ Support me in making new and better gems:[0m [31;4mhttp://pledgie.com/campaign
|
|
57
52
|
|
58
53
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
59
54
|
s.add_runtime_dependency(%q<shared-mime-info>, [">= 0"])
|
60
|
-
s.add_runtime_dependency(%q<
|
55
|
+
s.add_runtime_dependency(%q<mail>, [">= 2.2.1"])
|
61
56
|
else
|
62
57
|
s.add_dependency(%q<shared-mime-info>, [">= 0"])
|
63
|
-
s.add_dependency(%q<
|
58
|
+
s.add_dependency(%q<mail>, [">= 2.2.1"])
|
64
59
|
end
|
65
60
|
else
|
66
61
|
s.add_dependency(%q<shared-mime-info>, [">= 0"])
|
67
|
-
s.add_dependency(%q<
|
62
|
+
s.add_dependency(%q<mail>, [">= 2.2.1"])
|
68
63
|
end
|
69
64
|
end
|
70
65
|
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- BehindLogic
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-14 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -30,17 +30,17 @@ dependencies:
|
|
30
30
|
type: :runtime
|
31
31
|
version_requirements: *id001
|
32
32
|
- !ruby/object:Gem::Dependency
|
33
|
-
name:
|
33
|
+
name: mail
|
34
34
|
prerelease: false
|
35
35
|
requirement: &id002 !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
segments:
|
40
|
-
- 1
|
41
40
|
- 2
|
42
|
-
-
|
43
|
-
|
41
|
+
- 2
|
42
|
+
- 1
|
43
|
+
version: 2.2.1
|
44
44
|
type: :runtime
|
45
45
|
version_requirements: *id002
|
46
46
|
description: A Rubyesque interface to Gmail, with all the tools you'll need. Search, read and send multipart emails; archive, mark as read/unread, delete emails; and manage labels.
|
@@ -62,11 +62,6 @@ files:
|
|
62
62
|
- lib/gmail.rb
|
63
63
|
- lib/gmail/mailbox.rb
|
64
64
|
- lib/gmail/message.rb
|
65
|
-
- lib/ietf/rfc2045.rb
|
66
|
-
- lib/ietf/rfc822.rb
|
67
|
-
- lib/mime/entity.rb
|
68
|
-
- lib/mime/entity_tmail.rb
|
69
|
-
- lib/mime/message.rb
|
70
65
|
- lib/smtp_tls.rb
|
71
66
|
- ruby-gmail.gemspec
|
72
67
|
- test/test_gmail.rb
|
data/lib/ietf/rfc2045.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'ietf/rfc822'
|
2
|
-
module IETF
|
3
|
-
module RFC2045
|
4
|
-
def self.parse_rfc2045_from(raw)
|
5
|
-
headers, raw = IETF::RFC822.parse_rfc822_from(raw)
|
6
|
-
|
7
|
-
if headers['content-type'] =~ /multipart\/(\w+); boundary=([\"\'])(.*)\2/ || headers['content-type'] =~ /multipart\/(\w+); boundary(=)(.*)$/
|
8
|
-
content = {}
|
9
|
-
content[:type] = $1
|
10
|
-
content[:boundary] = $3
|
11
|
-
content[:content] = IETF::RFC2045.parse_rfc2045_content_from(raw, content[:boundary])
|
12
|
-
else
|
13
|
-
content = raw
|
14
|
-
end
|
15
|
-
|
16
|
-
return [headers, content]
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.parse_rfc2045_content_from(raw, boundary)
|
20
|
-
parts = ("\r\n" + raw).split(/#{CRLF.source}--#{boundary}(?:--)?(?:#{CRLF.source}|$)/)
|
21
|
-
parts.reject! {|p| p.gsub(/^[ \r\n#{HTAB}]?$/,'') == ''} # Remove any parts that are blank
|
22
|
-
puts "[RFC2045] PARTS:\n\t#{parts.map {|p| p.gsub(/\n/,"\n\t")}.join("\n---\n\t")}" if $DEBUG
|
23
|
-
parts.collect {|part|
|
24
|
-
puts "[RFC2045] Parsing PART with boundary #{boundary.inspect}:\n\t#{part.gsub(/\n/,"\n\t")}" if $DEBUG
|
25
|
-
IETF::RFC2045.parse_rfc2045_from(part)
|
26
|
-
}
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/ietf/rfc822.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
module IETF
|
2
|
-
CRLF = /\r\n/ # We can be a little lax about those carriage-returns.
|
3
|
-
TEXT = /.+?(?=#{CRLF.source}|$)/
|
4
|
-
HTAB = Regexp.new(11.chr)
|
5
|
-
LWSP_CHAR = /[ \t#{HTAB.source}]/
|
6
|
-
CTL = Regexp.new([(0...37).to_a,177].flatten.map {|i| i.chr}.join)
|
7
|
-
FIELD_BODY = /#{TEXT.source}(?:#{CRLF.source}#{LWSP_CHAR.source}#{TEXT.source})*/
|
8
|
-
FIELD_NAME = /^[^#{CTL.source} :]+/
|
9
|
-
FIELD = /(#{FIELD_NAME.source}):\s*(#{FIELD_BODY.source})/
|
10
|
-
module RFC822
|
11
|
-
def self.parse_rfc822_from(raw)
|
12
|
-
headers = {}
|
13
|
-
# Parse out rfc822 (headers)
|
14
|
-
head, remaining_raw = raw.split(/#{CRLF.source}#{CRLF.source}/,2)
|
15
|
-
puts "[RFC822] HEAD found:\n\t#{head.gsub(/\n/,"\n\t")}" if $DEBUG
|
16
|
-
head.scan(FIELD) do |field_name, field_body|
|
17
|
-
headers[field_name.downcase] = field_body
|
18
|
-
end
|
19
|
-
return [headers, remaining_raw]
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
data/lib/mime/entity.rb
DELETED
@@ -1,202 +0,0 @@
|
|
1
|
-
require 'ietf/rfc2045'
|
2
|
-
String::ALPHANUMERIC_CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a unless defined? String::ALPHANUMERIC_CHARACTERS
|
3
|
-
def String.random(size)
|
4
|
-
length = String::ALPHANUMERIC_CHARACTERS.length
|
5
|
-
(0...size).collect { String::ALPHANUMERIC_CHARACTERS[Kernel.rand(length)] }.join
|
6
|
-
end
|
7
|
-
|
8
|
-
module MIME
|
9
|
-
def self.capitalize_header(header_name)
|
10
|
-
header_name.gsub(/^(\w)/) {|m| m.capitalize}.gsub(/-(\w)/) {|n| '-' + n[1].chr.capitalize}
|
11
|
-
end
|
12
|
-
class Entity
|
13
|
-
def initialize(one=nil,two=nil)
|
14
|
-
if one.is_a?(Hash) || two.is_a?(Hash) # Intent is to generate a message from parameters
|
15
|
-
@headers = one.is_a?(Hash) ? one : two
|
16
|
-
set_content one if one.is_a?(String)
|
17
|
-
@encoding = 'quoted-printable' unless encoding
|
18
|
-
elsif one.is_a?(String) # Intent is to parse a message body
|
19
|
-
@raw = one.gsub(/\r/,'').gsub(/\n/, "\r\n") # normalizes end-of-line characters
|
20
|
-
from_parsed(IETF::RFC2045.parse_rfc2045_from(@raw))
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def inspect
|
25
|
-
"<#{self.class.name}##{object_id} Headers:{#{headers.collect {|k,v| "#{k}=#{v}"}.join(' ')}} content:#{multipart? ? 'multipart' : 'flat'}>"
|
26
|
-
end
|
27
|
-
|
28
|
-
def parsed
|
29
|
-
IETF::RFC2045.parse_rfc2045_from(@raw)
|
30
|
-
end
|
31
|
-
|
32
|
-
# This means we have a structure from IETF::RFC2045.
|
33
|
-
# Entity is: [headers, content], while content may be an array of Entities.
|
34
|
-
# Or, {:type, :boundary, :content}
|
35
|
-
def from_parsed(parsed)
|
36
|
-
case parsed
|
37
|
-
when Array
|
38
|
-
if parsed[0].is_a?(Hash) && (parsed[1].is_a?(Hash) || parsed[1].is_a?(String))
|
39
|
-
@headers = parsed[0]
|
40
|
-
@content = parsed[1].is_a?(Hash) ? parsed[1][:content].collect {|p| Entity.new.from_parsed(p)} : parsed[1]
|
41
|
-
if parsed[1].is_a?(Hash)
|
42
|
-
@multipart_type = parsed[1][:type]
|
43
|
-
@multipart_boundary = parsed[1][:boundary]
|
44
|
-
raise "IETF PARSING FAIL! (empty boundary)" if @multipart_boundary == ''
|
45
|
-
end
|
46
|
-
else
|
47
|
-
raise "IETF PARSING FAIL! ('A' structure)"
|
48
|
-
end
|
49
|
-
return self
|
50
|
-
when Hash
|
51
|
-
if parsed.has_key?(:type) && parsed.has_key?(:boundary) && parsed.has_key?(:content)
|
52
|
-
@content = parsed[:content].is_a?(Array) ? parsed[:content].collect {|p| Entity.new.from_parsed(p)} : parsed[:content]
|
53
|
-
else
|
54
|
-
raise "IETF PARSING FAIL! ('H' structure)"
|
55
|
-
end
|
56
|
-
return self
|
57
|
-
end
|
58
|
-
raise ArgumentError, "Must pass in either: [an array with two elements: headers(hash) and content(string or array)] OR [a hash containing :type, :boundary, and :content(being the former or a string)]"
|
59
|
-
end
|
60
|
-
|
61
|
-
##############
|
62
|
-
# ATTRIBUTES #
|
63
|
-
|
64
|
-
# An Entity has Headers.
|
65
|
-
def headers
|
66
|
-
@headers ||= {}
|
67
|
-
end
|
68
|
-
# An Entity has Content.
|
69
|
-
# IF the Content-Type is a multipart type,
|
70
|
-
# the content will be one or more Entities.
|
71
|
-
attr_reader :content, :multipart_type
|
72
|
-
|
73
|
-
#################
|
74
|
-
# Macro Methods #
|
75
|
-
|
76
|
-
def multipart?
|
77
|
-
!!(headers['content-type'] =~ /multipart\//) if headers['content-type']
|
78
|
-
end
|
79
|
-
def multipart_type
|
80
|
-
if headers['content-type'] =~ /multipart\/(\w+)/
|
81
|
-
$1
|
82
|
-
end if headers['content-type']
|
83
|
-
end
|
84
|
-
# Auto-generates a boundary if one doesn't yet exist.
|
85
|
-
def multipart_boundary
|
86
|
-
return nil unless multipart?
|
87
|
-
@multipart_boundary || begin
|
88
|
-
# Content-Type: multipart/mixed; boundary=000e0cd28d1282f4ba04788017e5
|
89
|
-
@multipart_boundary = String.random(25)
|
90
|
-
headers['content-type'] = "multipart/#{multipart_type}; boundary=#{@multipart_boundary}"
|
91
|
-
@multipart_boundary
|
92
|
-
end
|
93
|
-
end
|
94
|
-
def attachment?
|
95
|
-
headers['content-disposition'] =~ /^attachment(?=;|$)/ || headers['content-disposition'] =~ /^form-data;.* filename=[\"\']?[^\"\']+[\"\']?/ if headers['content-disposition']
|
96
|
-
end
|
97
|
-
alias :file? :attachment?
|
98
|
-
def part_filename
|
99
|
-
# Content-Disposition: attachment; filename="summary.txt"
|
100
|
-
if headers['content-disposition'] =~ /; filename=[\"\']?([^\"\']+)/
|
101
|
-
$1
|
102
|
-
end if headers['content-disposition']
|
103
|
-
end
|
104
|
-
|
105
|
-
def encoding
|
106
|
-
@encoding ||= headers['content-transfer-encoding'] || nil
|
107
|
-
end
|
108
|
-
attr_writer :encoding
|
109
|
-
alias :set_encoding :encoding=
|
110
|
-
|
111
|
-
def find_part(options)
|
112
|
-
find_parts(options).first
|
113
|
-
end
|
114
|
-
def find_parts(options)
|
115
|
-
parts = []
|
116
|
-
return nil unless (options[:content_type] && headers['content-type']) || (options[:content_disposition] && headers['content-disposition'])
|
117
|
-
# Do I match your search?
|
118
|
-
iam = true
|
119
|
-
iam = false if options[:content_type] && headers['content-type'] !~ /^#{options[:content_type]}(?=;|$)/
|
120
|
-
iam = false if options[:content_disposition] && headers['content-disposition'] !~ /^#{options[:content_disposition]}(?=;|$)/
|
121
|
-
parts << self if iam
|
122
|
-
# Do any of my children match your search?
|
123
|
-
content.each do |part|
|
124
|
-
parts.concat part.find_parts(options)
|
125
|
-
end if multipart?
|
126
|
-
return parts
|
127
|
-
end
|
128
|
-
|
129
|
-
def save_to_file(path=nil)
|
130
|
-
filename = path if path && !File.exists?(path) # If path doesn't exist, assume it's a filename
|
131
|
-
filename ||= path + '/' + part_filename if path && attachment? # If path does exist, and we're saving an attachment, use the attachment filename
|
132
|
-
filename ||= (attachment? ? part_filename : path) # If there is no path and we're saving an attachment, use the attachment filename; otherwise use path (whether it is present or not)
|
133
|
-
filename ||= '.' # No path supplied, and not saving an attachment. We'll just save it in the current directory.
|
134
|
-
if File.directory?(filename)
|
135
|
-
i = 0
|
136
|
-
begin
|
137
|
-
i += 1
|
138
|
-
filename = filename + "/attachment-#{i}"
|
139
|
-
end until !File.exists(filename)
|
140
|
-
end
|
141
|
-
# After all that trouble to get a filename to save to...
|
142
|
-
File.open(filename, 'w') do |file|
|
143
|
-
file << decoded_content
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
##########################
|
148
|
-
# CONVERTING / RENDERING #
|
149
|
-
|
150
|
-
# Renders this data structure into a string, encoded
|
151
|
-
def to_s
|
152
|
-
multipart_boundary # initialize the boundary if necessary
|
153
|
-
|
154
|
-
headers_string = {'charset' => 'utf-8', 'content-transfer-encoding' => @encoding} \
|
155
|
-
.merge(headers) \
|
156
|
-
.inject('') {|a,(k,v)| a << "#{MIME.capitalize_header(k)}: #{v}\r\n" }
|
157
|
-
|
158
|
-
content_string = content.is_a?(Array) ?
|
159
|
-
content.to_s :
|
160
|
-
content.collect {|part| "--#{multipart_boundary}\r\n#{part.to_s}\r\n" }.join + "--#{multipart_boundary}--\r\n"
|
161
|
-
|
162
|
-
[headers_string, content_string].join("\r\n")
|
163
|
-
end
|
164
|
-
|
165
|
-
# Converts this data structure into a string, but decoded if necessary
|
166
|
-
def decoded_content
|
167
|
-
return nil if @content.is_a?(Array)
|
168
|
-
case encoding.to_s.downcase
|
169
|
-
when 'quoted-printable'
|
170
|
-
@content.unpack('M')[0]
|
171
|
-
when 'base64'
|
172
|
-
@content.unpack('m')[0]
|
173
|
-
# when '7bit'
|
174
|
-
# # should get this 7bit encoding done too...
|
175
|
-
else
|
176
|
-
@content
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# You can set new content, and it will be saved in encoded form.
|
181
|
-
def set_content(raw)
|
182
|
-
@content = raw.is_a?(Array) ? raw :
|
183
|
-
case encoding.to_s.downcase
|
184
|
-
when 'quoted-printable'
|
185
|
-
[raw].pack('M')
|
186
|
-
when 'base64'
|
187
|
-
[raw].pack('m')
|
188
|
-
else
|
189
|
-
raw
|
190
|
-
end
|
191
|
-
end
|
192
|
-
alias :content= :set_content
|
193
|
-
|
194
|
-
private
|
195
|
-
def transfer_to(other)
|
196
|
-
other.instance_variable_set(:@content, @content.dup) if @content
|
197
|
-
other.headers.clear
|
198
|
-
other.headers.merge!(Hash[*headers.dup.select {|k,v| k =~ /content/}.flatten])
|
199
|
-
end
|
200
|
-
|
201
|
-
end
|
202
|
-
end
|
data/lib/mime/entity_tmail.rb
DELETED
@@ -1,219 +0,0 @@
|
|
1
|
-
require 'tmail'
|
2
|
-
require 'ietf/rfc2045'
|
3
|
-
String::ALPHANUMERIC_CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a unless defined? String::ALPHANUMERIC_CHARACTERS
|
4
|
-
def String.random(size)
|
5
|
-
length = String::ALPHANUMERIC_CHARACTERS.length
|
6
|
-
(0...size).collect { String::ALPHANUMERIC_CHARACTERS[Kernel.rand(length)] }.join
|
7
|
-
end
|
8
|
-
|
9
|
-
module MIME
|
10
|
-
def self.capitalize_header(header_name)
|
11
|
-
header_name.gsub(/^(\w)/) {|m| m.capitalize}.gsub(/-(\w)/) {|n| '-' + n[1].chr.capitalize}
|
12
|
-
end
|
13
|
-
class Entity
|
14
|
-
def initialize(one=nil,two=nil)
|
15
|
-
if one.is_a?(Hash) || two.is_a?(Hash) # Intent is to generate a message from parameters
|
16
|
-
@headers = one.is_a?(Hash) ? one : two
|
17
|
-
set_content one if one.is_a?(String)
|
18
|
-
@encoding = 'quoted-printable' unless encoding
|
19
|
-
elsif one.is_a?(String) # Intent is to parse a message body
|
20
|
-
@raw = one.gsub(/\r/,'').gsub(/\n/, "\r\n") # normalizes end-of-line characters
|
21
|
-
@tmail = TMail::Mail.parse(@raw)
|
22
|
-
from_tmail(@tmail)
|
23
|
-
elsif one.is_a?(TMail::Mail)
|
24
|
-
@tmail = one
|
25
|
-
from_tmail(@tmail)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def inspect
|
30
|
-
"<#{self.class.name}##{object_id} Headers:{#{headers.collect {|k,v| "#{k}=#{v}"}.join(' ')}} content:#{multipart? ? 'multipart' : 'flat'}>"
|
31
|
-
end
|
32
|
-
|
33
|
-
# This means we have a structure from IETF::RFC2045.
|
34
|
-
# Entity is: [headers, content], while content may be an array of Entities.
|
35
|
-
# Or, {:type, :boundary, :content}
|
36
|
-
def from_parsed(parsed)
|
37
|
-
case parsed
|
38
|
-
when Array
|
39
|
-
if parsed[0].is_a?(Hash) && (parsed[1].is_a?(Hash) || parsed[1].is_a?(String))
|
40
|
-
@headers = parsed[0]
|
41
|
-
@content = parsed[1].is_a?(Hash) ? parsed[1][:content].collect {|p| Entity.new.from_parsed(p)} : parsed[1]
|
42
|
-
if parsed[1].is_a?(Hash)
|
43
|
-
@multipart_type = parsed[1][:type]
|
44
|
-
@multipart_boundary = parsed[1][:boundary]
|
45
|
-
raise "IETF PARSING FAIL! (empty boundary)" if @multipart_boundary == ''
|
46
|
-
end
|
47
|
-
else
|
48
|
-
raise "IETF PARSING FAIL! ('A' structure)"
|
49
|
-
end
|
50
|
-
return self
|
51
|
-
when Hash
|
52
|
-
if parsed.has_key?(:type) && parsed.has_key?(:boundary) && parsed.has_key?(:content)
|
53
|
-
@content = parsed[:content].is_a?(Array) ? parsed[:content].collect {|p| Entity.new.from_parsed(p)} : parsed[:content]
|
54
|
-
else
|
55
|
-
raise "IETF PARSING FAIL! ('H' structure)"
|
56
|
-
end
|
57
|
-
return self
|
58
|
-
end
|
59
|
-
raise ArgumentError, "Must pass in either: [an array with two elements: headers(hash) and content(string or array)] OR [a hash containing :type, :boundary, and :content(being the former or a string)]"
|
60
|
-
end
|
61
|
-
|
62
|
-
# Parse a tmail object into our Entity object
|
63
|
-
def from_tmail(tmail)
|
64
|
-
raise ArgumentError, "Expecting a TMail::Mail object." unless tmail.is_a?(TMail::Mail)
|
65
|
-
@headers ||= Hash.new {|h,k| tmail.header[k].to_s }
|
66
|
-
if multipart?
|
67
|
-
@content = tmail.parts.collect { |tpart| Entity.new.from_tmail(tpart) }
|
68
|
-
else
|
69
|
-
set_content tmail.body # TMail has already decoded it, but we need it still encoded
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
##############
|
74
|
-
# ATTRIBUTES #
|
75
|
-
|
76
|
-
# An Entity has Headers.
|
77
|
-
def headers
|
78
|
-
@headers ||= {}
|
79
|
-
end
|
80
|
-
# An Entity has Content.
|
81
|
-
# IF the Content-Type is a multipart type,
|
82
|
-
# the content will be one or more Entities.
|
83
|
-
attr_reader :content, :multipart_type
|
84
|
-
|
85
|
-
#################
|
86
|
-
# Macro Methods #
|
87
|
-
|
88
|
-
def multipart?
|
89
|
-
return @tmail.multipart? if @tmail
|
90
|
-
!!(headers['content-type'] =~ /multipart\//) if headers['content-type']
|
91
|
-
end
|
92
|
-
def multipart_type
|
93
|
-
if headers['content-type'] =~ /multipart\/(\w+)/
|
94
|
-
$1
|
95
|
-
end if headers['content-type']
|
96
|
-
end
|
97
|
-
# Auto-generates a boundary if one doesn't yet exist.
|
98
|
-
def multipart_boundary
|
99
|
-
return nil unless multipart?
|
100
|
-
unless @multipart_boundary ||= (@tmail && @tmail.header['content-type'] ? @tmail.header['content-type'].params['boundary'] : nil)
|
101
|
-
# Content-Type: multipart/mixed; boundary=000e0cd28d1282f4ba04788017e5
|
102
|
-
@multipart_boundary = String.random(25)
|
103
|
-
headers['content-type'] = "multipart/#{multipart_type}; boundary=#{@multipart_boundary}"
|
104
|
-
@multipart_boundary
|
105
|
-
end
|
106
|
-
@multipart_boundary
|
107
|
-
end
|
108
|
-
def attachment?
|
109
|
-
@tmail.disposition_is_attachment? || headers['content-disposition'] =~ /^form-data;.* filename=[\"\']?[^\"\']+[\"\']?/ if headers['content-disposition']
|
110
|
-
end
|
111
|
-
alias :file? :attachment?
|
112
|
-
def part_filename
|
113
|
-
# Content-Disposition: attachment; filename="summary.txt"
|
114
|
-
if headers['content-disposition'] =~ /; filename=[\"\']?([^\"\']+)/
|
115
|
-
$1
|
116
|
-
end if headers['content-disposition']
|
117
|
-
end
|
118
|
-
|
119
|
-
def encoding
|
120
|
-
@encoding ||= headers['content-transfer-encoding'] || nil
|
121
|
-
end
|
122
|
-
# Re-encodes content if necessary when a new encoding is set
|
123
|
-
def encoding=(new_encoding)
|
124
|
-
raw_content = decoded_content # nil if multipart
|
125
|
-
@encoding = new_encoding
|
126
|
-
set_content(raw_content) if raw_content # skips if multipart
|
127
|
-
@encoding
|
128
|
-
end
|
129
|
-
alias :set_encoding :encoding=
|
130
|
-
|
131
|
-
def find_part(options)
|
132
|
-
find_parts(options,true).first
|
133
|
-
end
|
134
|
-
def find_parts(options,find_only_one=false)
|
135
|
-
parts = []
|
136
|
-
return nil unless (options[:content_type] && headers['content-type']) || (options[:content_disposition] && headers['content-disposition'])
|
137
|
-
# Do I match your search?
|
138
|
-
iam = true
|
139
|
-
iam = false if options[:content_type] && headers['content-type'] !~ /^#{options[:content_type]}(?=;|$)/
|
140
|
-
iam = false if options[:content_disposition] && headers['content-disposition'] !~ /^#{options[:content_disposition]}(?=;|$)/
|
141
|
-
parts << self if iam
|
142
|
-
return parts unless parts.empty?
|
143
|
-
# Do any of my children match your search?
|
144
|
-
content.each do |part|
|
145
|
-
parts.concat part.find_parts(options,find_only_one)
|
146
|
-
return parts if !parts.empty? && find_only_one
|
147
|
-
end if multipart?
|
148
|
-
return parts
|
149
|
-
end
|
150
|
-
|
151
|
-
def save_to_file(path=nil)
|
152
|
-
filename = path if path && !File.exists?(path) # If path doesn't exist, assume it's a filename
|
153
|
-
filename ||= path + '/' + part_filename if path && attachment? # If path does exist, and we're saving an attachment, use the attachment filename
|
154
|
-
filename ||= (attachment? ? part_filename : path) # If there is no path and we're saving an attachment, use the attachment filename; otherwise use path (whether it is present or not)
|
155
|
-
filename ||= '.' # No path supplied, and not saving an attachment. We'll just save it in the current directory.
|
156
|
-
if File.directory?(filename)
|
157
|
-
i = 0
|
158
|
-
begin
|
159
|
-
i += 1
|
160
|
-
filename = filename + "/attachment-#{i}"
|
161
|
-
end until !File.exists(filename)
|
162
|
-
end
|
163
|
-
# After all that trouble to get a filename to save to...
|
164
|
-
File.open(filename, 'w') do |file|
|
165
|
-
file << decoded_content
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
##########################
|
170
|
-
# CONVERTING / RENDERING #
|
171
|
-
|
172
|
-
# Renders this data structure into a string, encoded
|
173
|
-
def to_s
|
174
|
-
multipart_boundary # initialize the boundary if necessary, so it will be included in the headers
|
175
|
-
headers.inject('') {|a,(k,v)| a << "#{MIME.capitalize_header(k)}: #{v}\r\n"} + "\r\n" + if content.is_a?(Array)
|
176
|
-
"\r\n--#{multipart_boundary}\r\n" + content.collect {|part| part.to_s }.join("\r\n--#{multipart_boundary}\r\n") + "\r\n--#{multipart_boundary}--\r\n"
|
177
|
-
else
|
178
|
-
content.to_s
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
# Converts this data structure into a string, but decoded if necessary
|
183
|
-
def decoded_content
|
184
|
-
return nil if @content.is_a?(Array)
|
185
|
-
case encoding.to_s.downcase
|
186
|
-
when 'quoted-printable'
|
187
|
-
@content.unpack('M')[0]
|
188
|
-
when 'base64'
|
189
|
-
@content.unpack('m')[0]
|
190
|
-
# when '7bit'
|
191
|
-
# # should get this 7bit encoding done too...
|
192
|
-
else
|
193
|
-
@content
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# You can set new content, and it will be saved in encoded form.
|
198
|
-
def set_content(raw)
|
199
|
-
@content = raw.is_a?(Array) ? raw :
|
200
|
-
case encoding.to_s.downcase
|
201
|
-
when 'quoted-printable'
|
202
|
-
[raw].pack('M')
|
203
|
-
when 'base64'
|
204
|
-
[raw].pack('m')
|
205
|
-
else
|
206
|
-
raw
|
207
|
-
end
|
208
|
-
end
|
209
|
-
alias :content= :set_content
|
210
|
-
|
211
|
-
private
|
212
|
-
def transfer_to(other)
|
213
|
-
other.instance_variable_set(:@content, @content.dup) if @content
|
214
|
-
other.headers.clear
|
215
|
-
other.headers.merge!(Hash[*headers.dup.select {|k,v| k =~ /content/}.flatten])
|
216
|
-
end
|
217
|
-
|
218
|
-
end
|
219
|
-
end
|
data/lib/mime/message.rb
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
require 'mime/entity_tmail'
|
2
|
-
module MIME
|
3
|
-
# A Message is really a MIME::Entity,
|
4
|
-
# but should be used for the outermost Entity, because
|
5
|
-
# it includes helper methods to access common data
|
6
|
-
# from an email message.
|
7
|
-
class Message < Entity
|
8
|
-
def self.generate
|
9
|
-
Message.new('content-type' => 'text/plain', 'mime-version' => '1.0')
|
10
|
-
end
|
11
|
-
|
12
|
-
def to(addressee=nil)
|
13
|
-
headers['to'] = addressee if addressee
|
14
|
-
headers['to'].match(/([A-Z0-9._%+-]+@[A-Z0-9._%+-]+\.[A-Z]+)/i)[1] if headers['to'].respond_to?(:match)
|
15
|
-
end
|
16
|
-
|
17
|
-
def subject(subj=nil)
|
18
|
-
headers['subject'] = subj if subj
|
19
|
-
headers['subject']
|
20
|
-
end
|
21
|
-
|
22
|
-
def from(dummy=nil)
|
23
|
-
raise "Can't set FROM address in the message - will be set automatically when the message is sent." if dummy
|
24
|
-
headers['from'].match(/([A-Z0-9._%+-]+@[A-Z0-9._%+-]+\.[A-Z]+)/i)[1] if headers['from'].respond_to?(:match)
|
25
|
-
end
|
26
|
-
|
27
|
-
def attachments
|
28
|
-
find_parts(:content_disposition => 'attachment')
|
29
|
-
end
|
30
|
-
|
31
|
-
def text
|
32
|
-
part = find_part(:content_type => 'text/plain')
|
33
|
-
part.content if part
|
34
|
-
end
|
35
|
-
def html
|
36
|
-
part = find_part(:content_type => 'text/html')
|
37
|
-
part.content if part
|
38
|
-
end
|
39
|
-
|
40
|
-
def save_attachments_to(path=nil)
|
41
|
-
attachments.each {|a| a.save_to_file(path) }
|
42
|
-
end
|
43
|
-
|
44
|
-
def generate_multipart(*content_types)
|
45
|
-
headers['content-type'] = 'multipart/alternative'
|
46
|
-
@content = content_types.collect { |content_type| Entity.new('content-type' => content_type) }
|
47
|
-
end
|
48
|
-
|
49
|
-
def attach_file(filename)
|
50
|
-
raise ArgumentError, "Currently the <filename> given must be a String (path to a file)." unless filename.is_a?(String)
|
51
|
-
short_filename = filename.match(/([^\\\/]+)$/)[1]
|
52
|
-
|
53
|
-
# Generate the attachment piece
|
54
|
-
require 'shared-mime-info'
|
55
|
-
attachment = Entity.new(
|
56
|
-
'content-type' => MIME.check(filename).type + "; \r\n name=\"#{short_filename}\"",
|
57
|
-
'content-disposition' => "attachment; \r\n filename=\"#{short_filename}\"",
|
58
|
-
'content-transfer-encoding' => 'base64'
|
59
|
-
)
|
60
|
-
attachment.content = File.read(filename)
|
61
|
-
|
62
|
-
# Enclose in a top-level multipart/mixed
|
63
|
-
if multipart? && multipart_type == 'mixed'
|
64
|
-
# If we're already dealing with a mixed multipart, all we have to do is add the attachment part
|
65
|
-
(@content ||= []) << attachment
|
66
|
-
else
|
67
|
-
# If there is no content yet, add a little message about the attachment(s).
|
68
|
-
set_content 'See attachment(s)' if @content.nil?
|
69
|
-
# Generate the new top-level multipart, transferring what is here already into a child object
|
70
|
-
new_content = Entity.new
|
71
|
-
# Whatever it is, since it's not multipart/mixed, transfer it into a child object and add the attachment.
|
72
|
-
transfer_to(new_content)
|
73
|
-
headers.reject! {|k,v| k =~ /content/}
|
74
|
-
headers['content-type'] = 'multipart/mixed'
|
75
|
-
headers.delete 'content-transfer-encoding' # because we don't need a transfer-encoding on a multipart package.
|
76
|
-
@content = [new_content, attachment]
|
77
|
-
end
|
78
|
-
|
79
|
-
attachment
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|