ruby-gmail 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|