ruby-gmail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig ADDED
Binary file
data/.autotest ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.0.1 / 2009-11-18
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,13 @@
1
+ .autotest
2
+ History.txt
3
+ lib/gmail/mailbox.rb
4
+ lib/gmail/message.rb
5
+ lib/gmail.rb
6
+ lib/ietf/rfc2045.rb
7
+ lib/ietf/rfc822.rb
8
+ lib/mime/entity.rb
9
+ lib/mime/message.rb
10
+ lib/smtp_tls.rb
11
+ Manifest.txt
12
+ Rakefile
13
+ README.txt
data/README.txt ADDED
@@ -0,0 +1,77 @@
1
+ = ruby-gmail
2
+
3
+ * http://github.com/dcparker/ruby-gmail
4
+
5
+ == DESCRIPTION:
6
+
7
+ A Rubyesque interface to Gmail. Connect to Gmail via IMAP and manipulate emails and labels. Send email with your Gmail account via SMTP. Includes full support for parsing and generating MIME messages.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Read emails via IMAP
12
+ * Full MIME parsing ability, with understanding of attachments
13
+ * Create, rename, and delete labels
14
+ * Label, archive, delete, mark as read/unread/spam
15
+ * Send emails via SMTP
16
+ * Full ability to generate MIME messages including inline images and attachments
17
+
18
+ == SYNOPSIS:
19
+
20
+ gmail = Gmail.new(username, password)
21
+ gmail.inbox.count # => {:unread => 2, :read => 41, }
22
+ unread = gmail.inbox.emails(:unread)
23
+ unread[0].archive!
24
+ unread[1].delete!
25
+ unread[2].move_to('FunStuff') # => Labels 'FunStuff' and removes from inbox
26
+ unread[3].message # => a MIME::Message, parsed from the email body
27
+ unread[3].mark(:read)
28
+ unread[3].message.attachments.length
29
+ unread[4].label('FunStuff') # => Just adds the label 'FunStuff'
30
+ unread[4].message.save_attachments_to('path/to/save/into')
31
+ unread[5].message.attachments[0].save_to_file('path/to/save/into')
32
+ unread[6].mark(:spam)
33
+
34
+ new_email = MIME::Message.generate
35
+ new_email.to "email@example.com"
36
+ new_email.subject "Having fun in Puerto Rico!"
37
+ plain, html = new_email.generate_multipart('text/plain', 'text/html')
38
+ plain.content = "Text of plain message."
39
+ html.content = "<p>Text of <em>html</em> message.</p>"
40
+ new_email.attach_file('some_image.dmg')
41
+ gmail.send_email(new_email)
42
+
43
+ == REQUIREMENTS:
44
+
45
+ * ruby
46
+ * net/smtp
47
+ * net/imap
48
+ * gem shared-mime-info
49
+
50
+ == INSTALL:
51
+
52
+ * [sudo] gem install ruby-gmail -s http://gemcutter.org
53
+
54
+ == LICENSE:
55
+
56
+ (The MIT License)
57
+
58
+ Copyright (c) 2009 BehindLogic.com
59
+
60
+ Permission is hereby granted, free of charge, to any person obtaining
61
+ a copy of this software and associated documentation files (the
62
+ 'Software'), to deal in the Software without restriction, including
63
+ without limitation the rights to use, copy, modify, merge, publish,
64
+ distribute, sublicense, and/or sell copies of the Software, and to
65
+ permit persons to whom the Software is furnished to do so, subject to
66
+ the following conditions:
67
+
68
+ The above copyright notice and this permission notice shall be
69
+ included in all copies or substantial portions of the Software.
70
+
71
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
72
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
73
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
74
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
75
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
76
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
77
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'ruby-gmail' do
7
+ developer 'Daniel Parker', 'gems@behindlogic.com'
8
+ extra_deps << ['shared-mime-info', '>= 0']
9
+ end
data/lib/gmail.rb ADDED
@@ -0,0 +1,95 @@
1
+ require 'net/imap'
2
+ require 'net/smtp'
3
+ require 'smtp_tls'
4
+
5
+ class Gmail
6
+ VERSION = '0.0.1'
7
+
8
+ attr_reader :imap
9
+
10
+ def initialize(username, password)
11
+ # This is to hide the username and password, not like it REALLY needs hiding, but ... you know.
12
+ meta = class << self
13
+ class << self
14
+ attr_accessor :username, :password
15
+ end
16
+ self
17
+ end
18
+ meta.username = username
19
+ meta.password = password
20
+ @imap = Net::IMAP.new('imap.gmail.com',993,true)
21
+ @connected = true if @imap.login(username, password)
22
+ at_exit { logout if @connected }
23
+ end
24
+
25
+ # Accessors for IMAP things
26
+ def mailbox(name)
27
+ mailboxes[name] ||= Mailbox.new(self, name)
28
+ end
29
+
30
+ # Accessors for Gmail things
31
+ def inbox
32
+ mailbox('inbox')
33
+ end
34
+ # Log out of gmail
35
+ def logout
36
+ @connected = false if @imap.logout
37
+ end
38
+
39
+ def in_mailbox(mailbox, &block)
40
+ raise ArgumentError, "Must provide a code block" unless block_given?
41
+ mailbox_stack << mailbox
42
+ unless @selected == mailbox.name
43
+ @gmail.imap.select(mailbox.name)
44
+ @selected = mailbox.name
45
+ end
46
+ value = block.arity == 1 ? block.call(mailbox) : block.call
47
+ mailbox_stack.pop
48
+ # Select previously selected mailbox if there is one
49
+ if mailbox_stack.last
50
+ @gmail.imap.select(mailbox_stack.last.name)
51
+ @selected = mailbox.name
52
+ end
53
+ return value
54
+ end
55
+
56
+ def open_smtp(&block)
57
+ raise ArgumentError, "This method is to be used with a block." unless block_given?
58
+ meta = class << self; self end
59
+ puts "Opening SMTP..."
60
+ Net::SMTP.start('smtp.gmail.com', 587, 'localhost.localdomain', meta.username, meta.password, 'plain', true) do |smtp|
61
+ puts "SMTP open."
62
+ block.call(lambda {|to, body|
63
+ from = meta.username =~ /@/ ? meta.username : meta.username + '@gmail.com'
64
+ puts "Sending from #{from} to #{to}:\n#{body}"
65
+ smtp.send_message(body, from, to)
66
+ })
67
+ puts "SMTP closing."
68
+ end
69
+ puts "SMTP closed."
70
+ end
71
+
72
+ def send_email(to, body=nil)
73
+ meta = class << self; self end
74
+ if to.is_a?(MIME::Message)
75
+ to.headers['from'] = meta.username =~ /@/ ? meta.username : meta.username + '@gmail.com'
76
+ body = to.to_s
77
+ to = to.to
78
+ end
79
+ raise ArgumentError, "Please supply (to, body) to Gmail#send_email" if body.nil?
80
+ open_smtp do |smtp|
81
+ smtp.call to, body
82
+ end
83
+ end
84
+
85
+ private
86
+ def mailboxes
87
+ @mailboxes ||= {}
88
+ end
89
+ def mailbox_stack
90
+ @mailbox_stack ||= []
91
+ end
92
+ end
93
+
94
+ require 'gmail/mailbox'
95
+ require 'gmail/message'
@@ -0,0 +1,37 @@
1
+ class Gmail
2
+ class Mailbox
3
+ attr_reader :name
4
+
5
+ def initialize(gmail, name)
6
+ @gmail = gmail
7
+ @name = name.is_a?(Symbol) ? name.to_s.upcase : name
8
+ end
9
+
10
+ def inspect
11
+ "<#Mailbox name=#{@name}>"
12
+ end
13
+
14
+ def to_s
15
+ name
16
+ end
17
+
18
+ # Method: emails
19
+ # Args: [ :all | :unread | :read ]
20
+ def emails(key = :all)
21
+ aliases = {
22
+ :all => ['ALL'],
23
+ :unread => ['UNSEEN'],
24
+ :read => ['SEEN']
25
+ }
26
+ # puts "Gathering #{(aliases[key] || key).inspect} messages for mailbox '#{name}'..."
27
+ @gmail.in_mailbox(name) do
28
+ @gmail.imap.uid_search(aliases[key] || key).collect { |uid| messages[uid] ||= Message.new(@gmail, self, uid) }
29
+ end
30
+ end
31
+
32
+ private
33
+ def messages
34
+ @messages ||= {}
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,81 @@
1
+ require 'mime/message'
2
+ class Gmail
3
+ class Message
4
+ def initialize(gmail, mailbox, uid)
5
+ @gmail = gmail
6
+ @mailbox = mailbox
7
+ @uid = uid
8
+ end
9
+ def inspect
10
+ "<#Message:#{object_id} mailbox=#{mailbox.name}#{' uid='+@uid if @uid}#{' message_id='+@message_id if @message_id}>"
11
+ end
12
+
13
+ # Auto IMAP info
14
+ def uid
15
+ @uid ||= @gmail.imap.uid_search(['HEADER', 'Message-ID', message_id])[0]
16
+ end
17
+ def message_id
18
+ @message_id || begin
19
+ @gmail.in_mailbox(@mailbox.name) do
20
+ @message_id = @gmail.imap.uid_fetch(@uid, ['ENVELOPE'])[0].attr['ENVELOPE'].message_id
21
+ end
22
+ end
23
+ @message_id
24
+ end
25
+ def body
26
+ @body ||= @gmail.in_mailbox(@mailbox.name) do
27
+ @gmail.imap.uid_fetch(uid, "RFC822")[0].attr["RFC822"]
28
+ end
29
+ end
30
+
31
+ # Parsed MIME message object
32
+ def message
33
+ @message ||= MIME::Message.new(body)
34
+ end
35
+
36
+ # IMAP Operations
37
+ def flag(flg)
38
+ @gmail.in_mailbox(@mailbox.name) do
39
+ @gmail.imap.uid_store(uid, "+FLAGS", [flg])
40
+ end
41
+ end
42
+ def unflag(flg)
43
+ @gmail.in_mailbox(@mailbox.name) do
44
+ @gmail.imap.uid_store(uid, "-FLAGS", [flg])
45
+ end
46
+ end
47
+
48
+ # Gmail Operations
49
+ def mark(flag)
50
+ case flag
51
+ when :read
52
+ flag(:Seen)
53
+ when :unread
54
+ unflag(:Seen)
55
+ when :deleted
56
+ flag(:Deleted)
57
+ when :spam
58
+ move_to('[Gmail]/Spam')
59
+ end
60
+ end
61
+ def delete!
62
+ @mailbox.messages.delete(uid)
63
+ flag(:Deleted)
64
+ end
65
+ def label(name)
66
+ @gmail.in_mailbox(@mailbox.name) do |m|
67
+ m.gmail.imap.uid_copy(uid, name)
68
+ end
69
+ end
70
+ # We're not sure of any 'labels' except the 'mailbox' we're in at the moment.
71
+ # Research whether we can find flags that tell which other labels this email is a part of.
72
+ # def remove_label(name)
73
+ # end
74
+ def move_to(name)
75
+ label(name) && delete!
76
+ end
77
+ def archive!
78
+ move_to('[Gmail]/All Mail')
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,25 @@
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=(.*)/
8
+ content = {}
9
+ content[:type] = $1
10
+ content[:boundary] = $2
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
+ raw.split(/#{CRLF.source}--#{boundary}(?:--)?(?:#{CRLF.source}|$)/).collect {|part|
21
+ IETF::RFC2045.parse_rfc2045_from(part)
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
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
+ head.scan(FIELD) do |field_name, field_body|
16
+ headers[field_name.downcase] = field_body
17
+ end
18
+ return [headers, remaining_raw]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,187 @@
1
+ String::ALPHANUMERIC_CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a unless defined? String::ALPHANUMERIC_CHARACTERS
2
+ def String.random(size)
3
+ length = String::ALPHANUMERIC_CHARACTERS.length
4
+ (0...size).collect { String::ALPHANUMERIC_CHARACTERS[Kernel.rand(length)] }.join
5
+ end
6
+
7
+ module MIME
8
+ def self.capitalize_header(header_name)
9
+ header_name.gsub(/^(\w)/) {|m| m.capitalize}.gsub(/-(\w)/) {|n| '-' + n[1].chr.capitalize}
10
+ end
11
+ class Entity
12
+ def initialize(arg=nil)
13
+ if arg.is_a?(String)
14
+ @raw = arg
15
+ from_parsed(IETF::RFC2045.parse_rfc2045_from(@raw))
16
+ elsif arg.is_a?(Hash)
17
+ @headers = arg
18
+ end
19
+ end
20
+
21
+ def inspect
22
+ "<#{self.class.name}##{object_id} Headers:{#{headers.collect {|k,v| "#{k}=#{v}"}.join(' ')}} content:#{multipart? ? 'multipart' : 'flat'}>"
23
+ end
24
+
25
+ def parsed
26
+ IETF::RFC2045.parse_rfc2045_from(@raw)
27
+ end
28
+
29
+ # This means we have a structure from IETF::RFC2045.
30
+ # Entity is: [headers, content], while content may be an array of Entities.
31
+ # Or, {:type, :boundary, :content}
32
+ def from_parsed(parsed)
33
+ case parsed
34
+ when Array
35
+ puts "Parsed: #{parsed[0].class.name}, #{parsed[1].class.name}"
36
+ if parsed[0].is_a?(Hash) && (parsed[1].is_a?(Hash) || parsed[1].is_a?(String))
37
+ @headers = parsed[0]
38
+ @content = parsed[1].is_a?(Hash) ? parsed[1][:content].collect {|p| Entity.new.from_parsed(p)} : parsed[1]
39
+ if parsed[1].is_a?(Hash)
40
+ @multipart_type = parsed[1][:type]
41
+ @multipart_boundary = parsed[1][:boundary]
42
+ end
43
+ else
44
+ puts " -> fail"
45
+ end
46
+ return self
47
+ when Hash
48
+ puts "Parsed: #{parsed.keys.sort.inspect}"
49
+ if parsed.has_key?(:type) && parsed.has_key?(:boundary) && parsed.has_key?(:content)
50
+ @content = parsed[:content].is_a?(Array) ? parsed[:content].collect {|p| Entity.new.from_parsed(p)} : parsed[:content]
51
+ else
52
+ puts " -> fail"
53
+ end
54
+ return self
55
+ end
56
+ 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)]"
57
+ end
58
+
59
+ ##############
60
+ # ATTRIBUTES #
61
+
62
+ # An Entity has Headers.
63
+ def headers
64
+ @headers ||= {}
65
+ end
66
+ # An Entity has Content.
67
+ # IF the Content-Type is a multipart type,
68
+ # the content will be one or more Entities.
69
+ attr_reader :content, :multipart_type
70
+
71
+ #################
72
+ # Macro Methods #
73
+
74
+ def multipart?
75
+ !!(headers['content-type'] =~ /multipart\//)
76
+ end
77
+ def multipart_type
78
+ if headers['content-type'] =~ /multipart\/(\w+)/
79
+ $1
80
+ end
81
+ end
82
+ # Auto-generates a boundary if one doesn't yet exist.
83
+ def multipart_boundary
84
+ return nil unless multipart?
85
+ @multipart_boundary || begin
86
+ # Content-Type: multipart/mixed; boundary=000e0cd28d1282f4ba04788017e5
87
+ @multipart_boundary = String.random(25)
88
+ headers['content-type'] = "multipart/#{multipart_type}; boundary=#{@multipart_boundary}"
89
+ @multipart_boundary
90
+ end
91
+ end
92
+ def attachment?
93
+ headers['content-disposition'] =~ /^attachment(?=;|$)/
94
+ end
95
+ def attachment_filename
96
+ # Content-Disposition: attachment; filename="summary.txt"
97
+ if headers['content-disposition'] =~ /^attachment; filename=[\"\']?([^\"\']+)/
98
+ $1
99
+ end
100
+ end
101
+ attr_accessor :encoding
102
+ def encoding
103
+ @encoding ||= headers['content-transfer-encoding'] || nil
104
+ end
105
+ def find_part(options)
106
+ find_parts(options).first
107
+ end
108
+ def find_parts(options)
109
+ parts = []
110
+ # Do I match your search?
111
+ iam = true
112
+ iam = false if options[:content_type] && headers['content-type'] !~ /^#{options[:content_type]}(?=;|$)/
113
+ iam = false if options[:content_disposition] && headers['content-disposition'] !~ /^#{options[:content_disposition]}(?=;|$)/
114
+ parts << self if iam
115
+ # Do any of my children match your search?
116
+ content.each do |part|
117
+ parts.concat part.find_parts(options)
118
+ end if multipart?
119
+ return parts
120
+ end
121
+
122
+ def save_to_file(path=nil)
123
+ filename = path if path && !File.exists?(path) # If path doesn't exist, assume it's a filename
124
+ filename ||= path + '/' + attachment_filename if path && attachment? # If path does exist, and we're saving an attachment, use the attachment filename
125
+ filename ||= (attachment? ? attachment_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)
126
+ filename ||= '.' # No path supplied, and not saving an attachment. We'll just save it in the current directory.
127
+ if File.directory?(filename)
128
+ i = 0
129
+ begin
130
+ i += 1
131
+ filename = filename + "/attachment-#{i}"
132
+ end until !File.exists(filename)
133
+ end
134
+ # After all that trouble to get a filename to save to...
135
+ File.open(filename, 'w') do |file|
136
+ file << decoded_content
137
+ end
138
+ end
139
+
140
+ ##########################
141
+ # CONVERTING / RENDERING #
142
+
143
+ # Renders this data structure into a string, encoded
144
+ def to_s
145
+ multipart_boundary # initialize the boundary if necessary
146
+ headers.inject('') {|a,(k,v)| a << "#{MIME.capitalize_header(k)}: #{v}\r\n"} + "\r\n" + if content.is_a?(Array)
147
+ "\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"
148
+ else
149
+ content.to_s
150
+ end
151
+ end
152
+
153
+ # Converts this data structure into a string, but decoded if necessary
154
+ def decoded_content
155
+ return nil if @content.is_a?(Array)
156
+ case encoding
157
+ when 'quoted-printable'
158
+ @content.unpack('M')[0]
159
+ when 'base64'
160
+ @content.unpack('m')[0]
161
+ else
162
+ @content
163
+ end
164
+ end
165
+
166
+ # You can set new content, and it will be saved in encoded form.
167
+ def content=(raw)
168
+ @content = raw.is_a?(Array) ? raw :
169
+ case encoding
170
+ when 'quoted-printable'
171
+ [raw].pack('M')
172
+ when 'base64'
173
+ [raw].pack('m')
174
+ else
175
+ raw
176
+ end
177
+ end
178
+
179
+ private
180
+ def transfer_to(other)
181
+ other.instance_variable_set(:@content, @content.dup)
182
+ other.headers.clear
183
+ other.headers.merge!(Hash[*headers.dup.select {|k,v| k =~ /content/}.flatten])
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,68 @@
1
+ require 'mime/entity'
2
+ require 'shared-mime-info'
3
+ module MIME
4
+ # A Message is really a MIME::Entity,
5
+ # but should be used for the outermost Entity, because
6
+ # it includes helper methods to access common data
7
+ # from an email message.
8
+ class Message < Entity
9
+ def self.generate
10
+ Message.new('date' => Time.now.strftime("%a, %d %b %Y %H:%M:%S %z"), 'mime-version' => '1.0')
11
+ end
12
+
13
+ def to(addressee=nil)
14
+ headers['to'] = addressee if addressee
15
+ headers['to'].match(/([A-Z0-9._%+-]+@[A-Z0-9._%+-]+\.[A-Z]+)/i)[1]
16
+ end
17
+
18
+ def subject(subj=nil)
19
+ headers['subject'] = subj if subj
20
+ headers['subject']
21
+ end
22
+
23
+ def from
24
+ headers['from'].match(/([A-Z0-9._%+-]+@[A-Z0-9._%+-]+\.[A-Z]+)/i)[1]
25
+ end
26
+
27
+ def attachments
28
+ find_parts(:content_disposition => 'attachment')
29
+ end
30
+
31
+ def save_attachments_to(path=nil)
32
+ attachments.each {|a| a.save_to_file(path) }
33
+ end
34
+
35
+ def generate_multipart(*content_types)
36
+ headers['content-type'] = 'multipart/alternative'
37
+ @content = content_types.collect { |content_type| Entity.new('content-type' => content_type) }
38
+ end
39
+
40
+ def attach_file(filename)
41
+ short_filename = filename.match(/([^\\\/]+)$/)[1]
42
+
43
+ # Generate the attachment piece
44
+ attachment = Entity.new(
45
+ 'content-type' => MIME.check(filename).type + "; \r\n name=\"#{short_filename}\"",
46
+ 'content-disposition' => "attachment; \r\n filename=\"#{short_filename}\"",
47
+ 'content-transfer-encoding' => 'base64'
48
+ )
49
+ attachment.content = File.read(filename)
50
+
51
+ # Enclose in a top-level multipart/mixed
52
+ if multipart? && multipart_type == 'mixed'
53
+ # If already enclosed, all we have to do is add the attachment part
54
+ (@content ||= []) << attachment
55
+ else
56
+ # Generate the new top-level multipart, transferring what is here already into a child object
57
+ new_content = Entity.new
58
+ # Whatever it is, since it's not multipart/mixed, transfer it into a child object and add the attachment.
59
+ transfer_to(new_content)
60
+ headers.reject! {|k,v| k =~ /content/}
61
+ headers['content-type'] = 'multipart/mixed'
62
+ @content = [new_content, attachment]
63
+ end
64
+
65
+ attachment
66
+ end
67
+ end
68
+ end
data/lib/smtp_tls.rb ADDED
@@ -0,0 +1,94 @@
1
+ require "openssl"
2
+ require "net/smtp"
3
+
4
+ Net::SMTP.class_eval do
5
+
6
+ def self.start( address, port = nil,
7
+ helo = 'localhost.localdomain',
8
+ user = nil, secret = nil, authtype = nil, use_tls = false,
9
+ &block) # :yield: smtp
10
+ new(address, port).start(helo, user, secret, authtype, use_tls, &block)
11
+ end
12
+
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
16
+ if block_given?
17
+ begin
18
+ send start_method, helo, user, secret, authtype
19
+ return yield(self)
20
+ ensure
21
+ do_finish
22
+ end
23
+ else
24
+ send start_method helo, user, secret, authtype
25
+ return self
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def do_tls_start(helodomain, user, secret, authtype)
32
+ raise IOError, 'SMTP session already started' if @started
33
+ if RUBY_VERSION == '1.8.6'
34
+ check_auth_args user, secret, authtype if user or secret
35
+ else
36
+ check_auth_args user, secret if user or secret
37
+ end
38
+
39
+ sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
40
+ @socket = Net::InternetMessageIO.new(sock)
41
+ @socket.read_timeout = 60 #@read_timeout
42
+ @socket.debug_output = STDERR #@debug_output
43
+
44
+ check_response(critical { recv_response() })
45
+ do_helo(helodomain)
46
+
47
+ raise 'openssl library not installed' unless defined?(OpenSSL)
48
+ starttls
49
+ ssl = OpenSSL::SSL::SSLSocket.new(sock)
50
+ ssl.sync_close = true
51
+ ssl.connect
52
+ @socket = Net::InternetMessageIO.new(ssl)
53
+ @socket.read_timeout = 60 #@read_timeout
54
+ @socket.debug_output = STDERR #@debug_output
55
+ do_helo(helodomain)
56
+
57
+ authenticate user, secret, authtype if user
58
+ @started = true
59
+ ensure
60
+ unless @started
61
+ # authentication failed, cancel connection.
62
+ @socket.close if not @started and @socket and not @socket.closed?
63
+ @socket = nil
64
+ end
65
+ end
66
+
67
+ def do_helo(helodomain)
68
+ begin
69
+ if @esmtp
70
+ ehlo helodomain
71
+ else
72
+ helo helodomain
73
+ end
74
+ rescue Net::ProtocolError
75
+ if @esmtp
76
+ @esmtp = false
77
+ @error_occured = false
78
+ retry
79
+ end
80
+ raise
81
+ end
82
+ end
83
+
84
+ def starttls
85
+ getok('STARTTLS')
86
+ end
87
+
88
+ def quit
89
+ begin
90
+ getok('QUIT')
91
+ rescue EOFError, OpenSSL::SSL::SSLError
92
+ end
93
+ end
94
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-gmail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Parker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMQ0wCwYDVQQDDARnZW1z
14
+ MRswGQYKCZImiZPyLGQBGRYLYmVoaW5kbG9naWMxEzARBgoJkiaJk/IsZAEZFgNj
15
+ b20wHhcNMDkxMTE5MDQ0NzIzWhcNMTAxMTE5MDQ0NzIzWjBBMQ0wCwYDVQQDDARn
16
+ ZW1zMRswGQYKCZImiZPyLGQBGRYLYmVoaW5kbG9naWMxEzARBgoJkiaJk/IsZAEZ
17
+ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDs+2PvSoBC5mCz
18
+ Nm95RXKOb/u+CmPA70DoG9cF0zipCie003Vm0DYL3Rtobcr32eMvHxkWoJ6xoz0I
19
+ h74yNKtHjTTCxj86HWBPsE6xVMxVCftClndjCyKsiMiqvvp1wDNO0FFK+6LijmL3
20
+ 2Xkp4brWq1JO92y9vYct34R7o2X//+nwZs+sss+EYhNdvdUJfWy7tA5dghGdLvRn
21
+ UhJJSAtTefkBCwO7bufLEt+n7wIRbiJJ5dDCwE3NIX4wUSrNeYwXGXA/Ybki+BUl
22
+ 3KJF9IC0XR9fY9DGF0FXBKrkfDlZRrnQOem2aIxeuln0KQLJXXJuDTQPHO+mK3EG
23
+ UPhR7IAHAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
24
+ BBQn32ZStKmqwFqs2vuglYzkvDzBZjANBgkqhkiG9w0BAQUFAAOCAQEAe3Z+iMmy
25
+ IX9ChQW4hNNb1HOpgCMc2RL+vUwku9WE95dZ+BE4A6mOYTj5JXdYf4R4Z2vavr+d
26
+ nwJWtXPeIBWxireb8gUU0DwqodYTpsmkj5LD1zIaZ59rXlwDA9O0V4fwE1iRG5MD
27
+ mB7m8fT8WNOeg4AfjH4aSiHI1+HX1RQkc7KFdLotKnCevzYU6Jza5VUbXyJ+yCEH
28
+ DFARN3mkfGI+18MRDEi39nK2O/bBd6Wf0cYPEKsGQjNNAIBtv9belepSMd1KKfQ2
29
+ L7j8CnNDCrsHDe7/251D85wSvTH4Q/41NE5ahdCkkHwzDJeyhXpmNuUSswdn7woz
30
+ teST6sOe8lUhZQ==
31
+ -----END CERTIFICATE-----
32
+
33
+ date: 2009-11-19 00:00:00 -05:00
34
+ default_executable:
35
+ dependencies:
36
+ - !ruby/object:Gem::Dependency
37
+ name: shared-mime-info
38
+ type: :runtime
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ - !ruby/object:Gem::Dependency
47
+ name: hoe
48
+ type: :development
49
+ version_requirement:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.3.3
55
+ version:
56
+ description: A Rubyesque interface to Gmail. Connect to Gmail via IMAP and manipulate emails and labels. Send email with your Gmail account via SMTP. Includes full support for parsing and generating MIME messages.
57
+ email:
58
+ - gems@behindlogic.com
59
+ executables: []
60
+
61
+ extensions: []
62
+
63
+ extra_rdoc_files:
64
+ - History.txt
65
+ - Manifest.txt
66
+ - README.txt
67
+ files:
68
+ - .autotest
69
+ - History.txt
70
+ - lib/gmail/mailbox.rb
71
+ - lib/gmail/message.rb
72
+ - lib/gmail.rb
73
+ - lib/ietf/rfc2045.rb
74
+ - lib/ietf/rfc822.rb
75
+ - lib/mime/entity.rb
76
+ - lib/mime/message.rb
77
+ - lib/smtp_tls.rb
78
+ - Manifest.txt
79
+ - Rakefile
80
+ - README.txt
81
+ has_rdoc: true
82
+ homepage: http://github.com/dcparker/ruby-gmail
83
+ licenses: []
84
+
85
+ post_install_message:
86
+ rdoc_options:
87
+ - --main
88
+ - README.txt
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: "0"
96
+ version:
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ version:
103
+ requirements: []
104
+
105
+ rubyforge_project: ruby-gmail
106
+ rubygems_version: 1.3.5
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: A Rubyesque interface to Gmail
110
+ test_files: []
111
+
metadata.gz.sig ADDED
Binary file