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.
@@ -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-fiancé").each do |email|
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('tmail', '>= 1.2.3')
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.1
1
+ 0.2.0
@@ -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
- meta = class << self
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
- # Accessors for IMAP things
30
- def mailbox(name)
31
- mailboxes[name] ||= Mailbox.new(self, name)
32
- end
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
- mailbox('inbox')
38
+ in_label('inbox')
37
39
  end
38
- # Accessor for @imap, but ensures that it's logged in first.
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
- (@imap.list("", "%") + @imap.list("[Gmail]/", "%")).inject([]) { |labels,label|
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
- def in_mailbox(mailbox, &block)
63
- raise ArgumentError, "Must provide a code block" unless block_given?
64
- mailbox_stack << mailbox
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 :in_label :in_mailbox
55
+ alias :mailbox :label
79
56
 
80
- def open_smtp(&block)
81
- raise ArgumentError, "This method is to be used with a block." unless block_given?
82
- meta = class << self; self end
83
- puts "Opening SMTP..."
84
- Net::SMTP.start('smtp.gmail.com', 587, 'localhost.localdomain', meta.username, meta.password, 'plain', true) do |smtp|
85
- puts "SMTP open."
86
- block.call(lambda {|to, body|
87
- from = meta.username
88
- puts "Sending from #{from} to #{to}:\n#{body}"
89
- smtp.send_message(body.to_s, from, to)
90
- })
91
- puts "SMTP closing."
92
- end
93
- puts "SMTP closed."
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 new_message
97
- MIME::Message.generate
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
- def send_email(to, body=nil)
101
- meta = class << self; self end
102
- if to.is_a?(MIME::Message)
103
- to.headers['from'] = meta.username
104
- to.headers['date'] = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
105
- body = to.to_s
106
- to = to.to
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
- raise ArgumentError, "Please supply (to, body) to Gmail#send_email" if body.nil?
109
- open_smtp do |smtp|
110
- smtp.call to, body
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'
@@ -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
@@ -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, use_tls = false ) # :yield: smtp
15
- start_method = use_tls ? :do_tls_start : :do_start
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)
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{ruby-gmail}
8
- s.version = "0.1.1"
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-11}
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: http://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<tmail>, [">= 1.2.3"])
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<tmail>, [">= 1.2.3"])
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<tmail>, [">= 1.2.3"])
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
- - 1
8
- - 1
9
- version: 0.1.1
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-11 00:00:00 -04:00
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: tmail
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
- - 3
43
- version: 1.2.3
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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