Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of http://github.com/fasta/mail into fasta-master

Conflicts:
	lib/mail/mail.rb
  • Loading branch information...
commit 170ffa334a259e5f2080cb07c62be939973e6cf6 2 parents d7f918d + 54c9533
Mikel Lindsaar mikel authored
1  .gitignore
View
@@ -8,3 +8,4 @@ mail.tmproj
spec/fixtures/emails/failed_emails/
.rvmrc
*.rbc
+*.swp
4 lib/mail/configuration.rb
View
@@ -53,6 +53,8 @@ def lookup_retriever_method(method)
Mail::POP3
when :pop3
Mail::POP3
+ when :imap
+ Mail::IMAP
else
method
end
@@ -64,4 +66,4 @@ def param_encode_language(value = nil)
end
-end
+end
4 lib/mail/mail.rb
View
@@ -187,6 +187,10 @@ def Mail.read_from_string(mail_as_string)
Mail.new(mail_as_string)
end
+ def Mail.connection(&block)
+ retriever_method.connection(&block)
+ end
+
# Initialize the observers and interceptors arrays
@@delivery_notification_observers = []
@@delivery_interceptors = []
176 lib/mail/network/retriever_methods/imap.rb
View
@@ -1,5 +1,39 @@
+# encoding: utf-8
+
module Mail
+ # The IMAP retriever allows to get the last, first or all emails from a POP3 server.
+ # Each email retrieved (RFC2822) is given as an instance of +Message+.
+ #
+ # While being retrieved, emails can be yielded if a block is given.
+ #
+ # === Example of retrieving Emails from GMail:
+ #
+ # Mail.defaults do
+ # retriever_method :imap, { :address => "imap.googlemail.com",
+ # :port => 993,
+ # :user_name => '<username>',
+ # :password => '<password>',
+ # :enable_ssl => true }
+ # end
+ #
+ # Mail.all #=> Returns an array of all emails
+ # Mail.first #=> Returns the first unread email
+ # Mail.last #=> Returns the first unread email
+ #
+ # You can also pass options into Mail.find to locate an email in your imap mailbox
+ # with the following options:
+ #
+ # mailbox: name of the mailbox used for email retrieval. The default is 'INBOX'.
+ # what: last or first emails. The default is :first.
+ # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
+ # count: number of emails to retrieve. The default value is 10. A value of 1 returns an
+ # instance of Message, not an array of Message instances.
+ #
+ # Mail.find(:what => :first, :count => 10, :order => :asc)
+ # #=> Returns the first 10 emails in ascending order
+ #
class IMAP
+ require 'net/imap'
def initialize(values)
self.settings = { :address => "localhost",
@@ -9,10 +43,144 @@ def initialize(values)
:authentication => nil,
:enable_ssl => false }.merge!(values)
end
-
- def IMAP.get_messages(&block)
- # To be implemented
+
+ attr_accessor :settings
+
+ # Get the oldest received email(s)
+ #
+ # Possible options:
+ # mailbox: mailbox to retrieve the oldest received email(s) from. The default is 'INBOX'.
+ # count: number of emails to retrieve. The default value is 1.
+ # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
+ # keys: keywords for the imap SEARCH command. Can be either a string holding the entire
+ # search string or a single-dimension array of search keywords and arguments.
+ #
+ def first(options={}, &block)
+ options ||= {}
+ options[:what] = :first
+ options[:count] ||= 1
+ find(options, &block)
+ end
+
+ # Get the most recent received email(s)
+ #
+ # Possible options:
+ # mailbox: mailbox to retrieve the most recent received email(s) from. The default is 'INBOX'.
+ # count: number of emails to retrieve. The default value is 1.
+ # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
+ # keys: keywords for the imap SEARCH command. Can be either a string holding the entire
+ # search string or a single-dimension array of search keywords and arguments.
+ #
+ def last(options={}, &block)
+ options ||= {}
+ options[:what] = :last
+ options[:count] ||= 1
+ find(options, &block)
+ end
+
+ # Get all emails.
+ #
+ # Possible options:
+ # mailbox: mailbox to retrieve all email(s) from. The default is 'INBOX'.
+ # count: number of emails to retrieve. The default value is 1.
+ # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
+ # keys: keywords for the imap SEARCH command. Can be either a string holding the entire
+ # search string or a single-dimension array of search keywords and arguments.
+ #
+ def all(options={}, &block)
+ options ||= {}
+ options[:count] = :all
+ options[:keys] = 'ALL'
+ find(options, &block)
end
+ # Find emails in a POP3 mailbox. Without any options, the 10 last received emails are returned.
+ #
+ # Possible options:
+ # mailbox: mailbox to search the email(s) in. The default is 'INBOX'.
+ # what: last or first emails. The default is :first.
+ # order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
+ # count: number of emails to retrieve. The default value is 10. A value of 1 returns an
+ # instance of Message, not an array of Message instances.
+ #
+ def find(options={}, &block)
+ options = validate_options(options)
+
+ start do |imap|
+ imap.select(options[:mailbox])
+
+ message_ids = imap.uid_search(options[:keys])
+ message_ids.reverse! if options[:what].to_sym == :last
+ message_ids = message_ids.first(options[:count]) if options[:count].is_a?(Integer)
+ message_ids.reverse! if (options[:what].to_sym == :last && options[:order].to_sym == :asc) ||
+ (options[:what].to_sym != :last && options[:order].to_sym == :desc)
+
+ if block_given?
+ message_ids.each do |message_id|
+ fetchdata = imap.uid_fetch(message_id, ['RFC822'])[0]
+
+ yield Mail.new(fetchdata.attr['RFC822'])
+ end
+ else
+ emails = []
+ message_ids.each do |message_id|
+ fetchdata = imap.uid_fetch(message_id, ['RFC822'])[0]
+
+ emails << Mail.new(fetchdata.attr['RFC822'])
+ end
+ emails.size == 1 && options[:count] == 1 ? emails.first : emails
+ end
+ end
+ end
+
+ # Delete all emails from a IMAP mailbox
+ def delete_all(mailbox='INBOX')
+ mailbox ||= 'INBOX'
+
+ start do |imap|
+ imap.select(mailbox)
+ imap.uid_search(['ALL']).each do |message_id|
+ imap.uid_store(message_id, "+FLAGS", [Net::IMAP::DELETED])
+ end
+ imap.expunge
+ end
+ end
+
+ # Returns the connection object of the retrievable (IMAP or POP3)
+ def connection(&block)
+ raise ArgumentError.new('Mail::Retrievable#connection takes a block') unless block_given?
+
+ start do |imap|
+ yield imap
+ end
+ end
+
+ private
+
+ # Set default options
+ def validate_options(options)
+ options ||= {}
+ options[:mailbox] ||= 'INBOX'
+ options[:count] ||= 10
+ options[:order] ||= :asc
+ options[:what] ||= :first
+ options[:keys] ||= 'ALL'
+ options
+ end
+
+ # Start an IMAP session and ensures that it will be closed in any case.
+ def start(config=Mail::Configuration.instance, &block)
+ raise ArgumentError.new("Mail::Retrievable#imap_start takes a block") unless block_given?
+
+ imap = Net::IMAP.new(settings[:address], settings[:port], settings[:enable_ssl], nil, false)
+ imap.login(settings[:user_name], settings[:password])
+
+ yield imap
+ ensure
+ if defined?(imap) && imap && !imap.disconnected?
+ imap.disconnect
+ end
+ end
+
end
-end
+end
11 lib/mail/network/retriever_methods/pop3.rb
View
@@ -151,6 +151,15 @@ def delete_all
end
end
end
+
+ # Returns the connection object of the retrievable (IMAP or POP3)
+ def connection(&block)
+ raise ArgumentError.new('Mail::Retrievable#connection takes a block') unless block_given?
+
+ start do |pop3|
+ yield pop3
+ end
+ end
private
@@ -182,4 +191,4 @@ def start(config = Configuration.instance, &block)
end
end
-end
+end
188 spec/mail/network/retriever_methods/imap_spec.rb
View
@@ -0,0 +1,188 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe "IMAP Retriever" do
+
+ before(:each) do
+ Mail.defaults do
+ retriever_method :imap, { :address => 'localhost',
+ :port => 993,
+ :user_name => nil,
+ :password => nil,
+ :enable_ssl => true }
+ end
+ end
+
+ describe "find with and without block" do
+ it "should find all emails with a given block" do
+ MockIMAP.should be_disconnected
+
+ messages = []
+ Mail.all do |message|
+ messages << message
+ end
+ messages.map { |m| m.raw_source }.sort.should == MockIMAP.examples.map { |m| m.attr['RFC822']}.sort
+
+ MockIMAP.should be_disconnected
+ end
+ it "should get all emails without a given block" do
+ MockIMAP.should be_disconnected
+
+ messages = Mail.all
+ messages.map { |m| m.raw_source }.sort.should == MockIMAP.examples.map { |m| m.attr['RFC822']}.sort
+
+ MockIMAP.should be_disconnected
+ end
+ end
+
+ describe "find and options" do
+ it "should handle the :count option" do
+ messages = Mail.find(:count => :all, :what => :last, :order => :asc)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }
+
+ message = Mail.find(:count => 1, :what => :last)
+ message.raw_source.should == MockIMAP.examples.last.attr['RFC822']
+
+ messages = Mail.find(:count => 2, :what => :last, :order => :asc)
+ messages[0..1].map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }[-2..-1]
+ end
+ it "should handle the :what option" do
+ messages = Mail.find(:count => :all, :what => :last)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }
+
+ messages = Mail.find(:count => 2, :what => :first, :order => :asc)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }[0..1]
+ end
+ it "should handle the :order option" do
+ messages = Mail.find(:order => :desc, :count => 5, :what => :last)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }[-5..-1].reverse
+
+ messages = Mail.find(:order => :asc, :count => 5, :what => :last)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }[-5..-1]
+ end
+ it "should handle the :mailbox option" do
+ messages = Mail.find(:mailbox => 'SOME-RANDOM-MAILBOX')
+
+ MockIMAP.mailbox.should == 'SOME-RANDOM-MAILBOX'
+ end
+ it "should find the last 10 messages by default" do
+ messages = Mail.find
+
+ messages.size.should == 10
+ end
+ it "should search the mailbox 'INBOX' by default" do
+ messages = Mail.find
+
+ MockIMAP.mailbox.should == 'INBOX'
+ end
+ end
+
+ describe "last" do
+ it "should find the last received messages" do
+ messages = Mail.last(:count => 5)
+
+ messages.should be_instance_of(Array)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822']}[-5..-1]
+ end
+ it "should find the last received message" do
+ message = Mail.last
+
+ message.raw_source.should == MockIMAP.examples.last.attr['RFC822']
+ end
+ end
+
+ describe "first" do
+ it "should find the first received messages" do
+ messages = Mail.first(:count => 5)
+
+ messages.should be_instance_of(Array)
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822']}[0..4]
+ end
+ it "should find the first received message" do
+ message = Mail.first
+
+ message.raw_source.should == MockIMAP.examples.first.attr['RFC822']
+ end
+ end
+
+ describe "all" do
+ it "should find all messages" do
+ messages = Mail.all
+
+ messages.size.should == MockIMAP.examples.size
+ messages.map { |m| m.raw_source }.should == MockIMAP.examples.map { |m| m.attr['RFC822'] }
+ end
+ end
+
+ describe "delete_all" do
+ it "should delete all messages" do
+ messages = Mail.all
+ Mail.delete_all
+
+ MockIMAP.examples.size.should == 0
+ end
+ end
+
+ describe "connection" do
+ it "should raise an Error if no block is given" do
+ lambda { Mail.connection { |m| raise ArgumentError.new } }.should raise_error
+ end
+ it "should yield the connection object to the given block" do
+ Mail.connection do |connection|
+ connection.should be_an_instance_of(MockIMAP)
+ end
+ end
+ end
+
+ describe "handling of options" do
+ it "should set default options" do
+ retrievable = Mail::IMAP.new({})
+ options = retrievable.send(:validate_options, {})
+
+ options[:count].should be_present
+ options[:count].should == 10
+
+ options[:order].should be_present
+ options[:order].should == :asc
+
+ options[:what].should be_present
+ options[:what].should == :first
+
+ options[:mailbox].should be_present
+ options[:mailbox].should == 'INBOX'
+ end
+ it "should not replace given configuration" do
+ retrievable = Mail::IMAP.new({})
+ options = retrievable.send(:validate_options, {
+ :mailbox => 'some/mail/box',
+ :count => 2,
+ :order => :asc,
+ :what => :first
+ })
+
+ options[:count].should be_present
+ options[:count].should == 2
+
+ options[:order].should be_present
+ options[:order].should == :asc
+
+ options[:what].should be_present
+ options[:what].should == :first
+
+ options[:mailbox].should be_present
+ options[:mailbox].should == 'some/mail/box'
+ end
+ end
+
+ describe "error handling" do
+ it "should finish the IMAP connection if an exception is raised" do
+ MockIMAP.should be_disconnected
+
+ lambda { Mail.all { |m| raise ArgumentError.new } }.should raise_error
+
+ MockIMAP.should be_disconnected
+ end
+ end
+
+end
+
11 spec/mail/network/retriever_methods/pop3_spec.rb
View
@@ -136,6 +136,17 @@
end
end
+ describe "connection" do
+ it "should raise an Error if no block is given" do
+ lambda { Mail.connection { |m| raise ArgumentError.new } }.should raise_error
+ end
+ it "should yield the connection object to the given block" do
+ Mail.connection do |connection|
+ connection.should be_an_instance_of(MockPOP3)
+ end
+ end
+ end
+
describe "handling of options" do
it "should set default options" do
71 spec/spec_helper.rb
View
@@ -165,3 +165,74 @@ def self.new(*args)
MockPOP3.new
end
end
+
+class MockIMAPFetchData
+ attr_reader :attr
+
+ def initialize(rfc822, number)
+ @attr = { 'RFC822' => rfc822 }
+ end
+
+end
+
+class MockIMAP
+ @@connection = false
+ @@mailbox = nil
+ @@marked_for_deletion = []
+
+ cattr_reader :examples
+
+ def initialize
+ @@examples = []
+ (0..19).each do |i|
+ @@examples << MockIMAPFetchData.new("test#{i.to_s.rjust(2, '0')}", i)
+ end
+ end
+
+ def login(user, password)
+ @@connection = true
+ end
+
+ def disconnect
+ @@connection = false
+ end
+
+ def select(mailbox)
+ @@mailbox = mailbox
+ end
+
+ def uid_search(keys, charset=nil)
+ [*(0..@@examples.size - 1)]
+ end
+
+ def uid_fetch(set, attr)
+ [@@examples[set]]
+ end
+
+ def uid_store(set, attr, flags)
+ if attr == "+FLAGS" && flags.include?(Net::IMAP::DELETED)
+ @@marked_for_deletion << set
+ end
+ end
+
+ def expunge
+ @@marked_for_deletion.reverse.each do |i| # start with highest index first
+ @@examples.delete_at(i)
+ end
+ @@marked_for_deletion = []
+ end
+
+ def self.mailbox; @@mailbox end # test only
+
+ def self.disconnected?; @@connection == false end
+ def disconnected?; @@connection == false end
+
+end
+
+require 'net/imap'
+class Net::IMAP
+ def self.new(*args)
+ MockIMAP.new
+ end
+end
+
Please sign in to comment.
Something went wrong with that request. Please try again.