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 +0 -0
- data/.autotest +23 -0
- data/History.txt +6 -0
- data/Manifest.txt +13 -0
- data/README.txt +77 -0
- data/Rakefile +9 -0
- data/lib/gmail.rb +95 -0
- data/lib/gmail/mailbox.rb +37 -0
- data/lib/gmail/message.rb +81 -0
- data/lib/ietf/rfc2045.rb +25 -0
- data/lib/ietf/rfc822.rb +21 -0
- data/lib/mime/entity.rb +187 -0
- data/lib/mime/message.rb +68 -0
- data/lib/smtp_tls.rb +94 -0
- metadata +111 -0
- metadata.gz.sig +0 -0
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
data/Manifest.txt
ADDED
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
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
|
data/lib/ietf/rfc2045.rb
ADDED
@@ -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
|
data/lib/ietf/rfc822.rb
ADDED
@@ -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
|
data/lib/mime/entity.rb
ADDED
@@ -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
|
data/lib/mime/message.rb
ADDED
@@ -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
|