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.
@@ -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