winton-fetcher 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +94 -0
- data/Rakefile +30 -0
- data/fetcher.gemspec +27 -0
- data/gemspec.rb +17 -0
- data/init.rb +0 -0
- data/lib/fetcher.rb +22 -0
- data/lib/fetcher/base.rb +63 -0
- data/lib/fetcher/imap.rb +79 -0
- data/lib/fetcher/pop.rb +50 -0
- data/lib/vendor/plain_imap.rb +21 -0
- data/lib/vendor/secure_pop.rb +125 -0
- data/test/fetcher_test.rb +74 -0
- metadata +69 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Dan Weinand and Slantwise Design, LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
= Fetcher
|
2
|
+
|
3
|
+
Fetcher is a simple message fetcher perfect for using in a daemon or via cron.
|
4
|
+
|
5
|
+
It implements the following common pattern:
|
6
|
+
|
7
|
+
1. Connect to a server
|
8
|
+
2. Download available messages
|
9
|
+
3. Send each message to another object for further processing
|
10
|
+
4. Remove downloaded messages from the remote server
|
11
|
+
|
12
|
+
Install using:
|
13
|
+
|
14
|
+
sudo gem install winton-fetcher
|
15
|
+
|
16
|
+
== Notice
|
17
|
+
|
18
|
+
This is a fork of {fetcher}[http://github.com/look/fetcher] by {Luke Francl}[http://github.com/look].
|
19
|
+
|
20
|
+
Differences:
|
21
|
+
|
22
|
+
* Gemified
|
23
|
+
* Doesn't have generators
|
24
|
+
* Fixes a connection exception issue
|
25
|
+
|
26
|
+
== Usage
|
27
|
+
|
28
|
+
Create a new fetcher object like the following:
|
29
|
+
|
30
|
+
@fetcher = Fetcher.create({:type => :pop,
|
31
|
+
:receiver => IncomingMailHandler,
|
32
|
+
:server => 'mail.example.com',
|
33
|
+
:username => 'jim',
|
34
|
+
:password => 'test'})
|
35
|
+
|
36
|
+
The receiver object is expected to have a receive method that takes a message as its only argument
|
37
|
+
(e.g., the way <tt>ActionMailer::Base.recieve</tt> works; but you don't <em>have</em> to use ActionMailer.).
|
38
|
+
|
39
|
+
Call <tt>fetch</tt> to download messages and process them.
|
40
|
+
|
41
|
+
@fetcher.fetch
|
42
|
+
|
43
|
+
== Configuration
|
44
|
+
|
45
|
+
The following options can be passed to the <tt>Fetcher.create</tt> factory method:
|
46
|
+
|
47
|
+
[<tt>type</tt>] POP or IMAP
|
48
|
+
[<tt>server</tt>] The IP address or domain name of the server
|
49
|
+
[<tt>port</tt>] The port to connect to (defaults to the standard port for the type of server)
|
50
|
+
[<tt>ssl</tt>] Set to any value to use SSL encryption
|
51
|
+
[<tt>username</tt>] The username used to connect to the server
|
52
|
+
[<tt>password</tt>] The password used to connect to the server
|
53
|
+
[<tt>authentication</tt>] The authentication scheme to use (IMAP only). Supports LOGIN, CRAM-MD5, and PASSWORD (defaults to PLAIN)
|
54
|
+
[<tt>use_login</tt>] Set to any value to use the LOGIN command instead of AUTHENTICATE. Some servers, like GMail, do not support AUTHENTICATE (IMAP only).
|
55
|
+
[<tt>sleep_time</tt>] The number of seconds to sleep between fetches (defaults to 60 seconds; valid only for the generated daemon)
|
56
|
+
[<tt>processed_folder</tt>] The name of a folder to move mail to after it has been processed (IMAP only). <b>NOTE:</b> If not specified, mail is deleted.
|
57
|
+
[<tt>error_folder</tt>] The name a folder to move mail that causes an error during processing (IMAP only). Defaults to +bogus+.
|
58
|
+
|
59
|
+
== Running via cron
|
60
|
+
|
61
|
+
You can also run the Fetcher periodically via cron. It is important to ensure that only one
|
62
|
+
instance is running at one time, and for that the {Lockfile gem}[http://codeforpeople.com/lib/ruby/lockfile/] is recommended.
|
63
|
+
|
64
|
+
Here is an example script to be with <tt>script/runner</tt> via cron:
|
65
|
+
|
66
|
+
begin
|
67
|
+
Lockfile.new('cron_mail_fetcher.lock', :retries => 0) do
|
68
|
+
config = YAML.load_file("#{RAILS_ROOT}/config/mail.yml")
|
69
|
+
config = config[RAILS_ENV].to_options
|
70
|
+
|
71
|
+
fetcher = Fetcher.create({:receiver => MailReceiver}.merge(config))
|
72
|
+
fetcher.fetch
|
73
|
+
end
|
74
|
+
rescue Lockfile::MaxTriesLockError => e
|
75
|
+
puts "Another fetcher is already running. Exiting."
|
76
|
+
end
|
77
|
+
|
78
|
+
== Extending
|
79
|
+
|
80
|
+
You can subclass <tt>Fetcher::Base</tt> or one of the protocol-specific classed to override the standard behavior.
|
81
|
+
|
82
|
+
== Further documentation
|
83
|
+
|
84
|
+
<shameless-plug>
|
85
|
+
|
86
|
+
You can read more about how to use the Fetcher in the PeepCode book {Receiving Email with Ruby}[https://peepcode.com/products/mms2r-pdf].
|
87
|
+
|
88
|
+
</shameless-plug>
|
89
|
+
|
90
|
+
== Credit & Copyright
|
91
|
+
|
92
|
+
Created by Dan Weinand and Luke Francl. Development supported by {Slantwise Design}[http://slantwisedesign.com].
|
93
|
+
|
94
|
+
Licensed under the terms of the MIT License. Be excellent to each other.
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'gemspec'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc "Generate gemspec"
|
10
|
+
task :gemspec do
|
11
|
+
File.open("#{Dir.pwd}/#{GEM_NAME}.gemspec", 'w') do |f|
|
12
|
+
f.write(GEM_SPEC.to_ruby)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Test the fetcher plugin.'
|
17
|
+
Rake::TestTask.new(:test) do |t|
|
18
|
+
t.libs << 'lib'
|
19
|
+
t.pattern = 'test/**/*_test.rb'
|
20
|
+
t.verbose = true
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Generate documentation for the fetcher plugin.'
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = 'rdoc'
|
26
|
+
rdoc.title = 'Fetcher'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
28
|
+
rdoc.rdoc_files.include('README')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
data/fetcher.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{fetcher}
|
5
|
+
s.version = "0.1.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Luke Francl"]
|
9
|
+
s.date = %q{2009-06-27}
|
10
|
+
s.email = %q{look@recursion.org}
|
11
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
12
|
+
s.files = ["fetcher.gemspec", "gemspec.rb", "init.rb", "lib", "lib/fetcher", "lib/fetcher/base.rb", "lib/fetcher/imap.rb", "lib/fetcher/pop.rb", "lib/fetcher.rb", "lib/vendor", "lib/vendor/plain_imap.rb", "lib/vendor/secure_pop.rb", "MIT-LICENSE", "Rakefile", "README.rdoc", "test", "test/fetcher_test.rb"]
|
13
|
+
s.homepage = %q{http://github.com/winton/fetcher}
|
14
|
+
s.require_paths = ["lib"]
|
15
|
+
s.rubygems_version = %q{1.3.1}
|
16
|
+
s.summary = %q{download email from POP3 or IMAP and do stuff with it. gemified fork}
|
17
|
+
|
18
|
+
if s.respond_to? :specification_version then
|
19
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
20
|
+
s.specification_version = 2
|
21
|
+
|
22
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
23
|
+
else
|
24
|
+
end
|
25
|
+
else
|
26
|
+
end
|
27
|
+
end
|
data/gemspec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
GEM_NAME = 'fetcher'
|
2
|
+
GEM_FILES = FileList['**/*'] - FileList['coverage', 'coverage/**/*', 'pkg', 'pkg/**/*']
|
3
|
+
GEM_SPEC = Gem::Specification.new do |s|
|
4
|
+
# == CONFIGURE ==
|
5
|
+
s.author = "Luke Francl"
|
6
|
+
s.email = "look@recursion.org"
|
7
|
+
s.homepage = "http://github.com/winton/#{GEM_NAME}"
|
8
|
+
s.summary = "download email from POP3 or IMAP and do stuff with it. gemified fork"
|
9
|
+
# == CONFIGURE ==
|
10
|
+
s.extra_rdoc_files = [ "README.rdoc" ]
|
11
|
+
s.files = GEM_FILES.to_a
|
12
|
+
s.has_rdoc = false
|
13
|
+
s.name = GEM_NAME
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.require_path = "lib"
|
16
|
+
s.version = "0.1.1"
|
17
|
+
end
|
data/init.rb
ADDED
File without changes
|
data/lib/fetcher.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Fetcher
|
2
|
+
# Use factory-style initialization or insantiate directly from a subclass
|
3
|
+
#
|
4
|
+
# Options:
|
5
|
+
# * <tt>:type</tt> - Name of class as a symbol to instantiate
|
6
|
+
#
|
7
|
+
# Other options are the same as Fetcher::Base.new
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# Fetcher.create(:type => :pop) is equivalent to
|
12
|
+
# Fetcher::Pop.new()
|
13
|
+
def self.create(options={})
|
14
|
+
klass = options.delete(:type)
|
15
|
+
raise ArgumentError, 'Must supply a type' unless klass
|
16
|
+
module_eval "#{klass.to_s.capitalize}.new(options)"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'fetcher/base'
|
21
|
+
require 'fetcher/pop'
|
22
|
+
require 'fetcher/imap'
|
data/lib/fetcher/base.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Fetcher
|
2
|
+
class Base
|
3
|
+
# Options:
|
4
|
+
# * <tt>:server</tt> - Server to connect to.
|
5
|
+
# * <tt>:username</tt> - Username to use when connecting to server.
|
6
|
+
# * <tt>:password</tt> - Password to use when connecting to server.
|
7
|
+
# * <tt>:receiver</tt> - Receiver object to pass messages to. Assumes the
|
8
|
+
# receiver object has a receive method that takes a message as it's argument
|
9
|
+
#
|
10
|
+
# Additional protocol-specific options implimented by sub-classes
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# Fetcher::Base.new(:server => 'mail.example.com',
|
14
|
+
# :username => 'pam',
|
15
|
+
# :password => 'test',
|
16
|
+
# :receiver => IncomingMailHandler)
|
17
|
+
def initialize(options={})
|
18
|
+
%w(server username password receiver).each do |opt|
|
19
|
+
raise ArgumentError, "#{opt} is required" unless options[opt.to_sym]
|
20
|
+
# convert receiver to a Class if it isn't already.
|
21
|
+
if opt == "receiver" && options[:receiver].is_a?(String)
|
22
|
+
options[:receiver] = Kernel.const_get(options[:receiver])
|
23
|
+
end
|
24
|
+
|
25
|
+
instance_eval("@#{opt} = options[:#{opt}]")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Run the fetching process
|
30
|
+
def fetch
|
31
|
+
establish_connection
|
32
|
+
get_messages
|
33
|
+
close_connection
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Stub. Should be overridden by subclass.
|
39
|
+
def establish_connection #:nodoc:
|
40
|
+
raise NotImplementedError, "This method should be overridden by subclass"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Stub. Should be overridden by subclass.
|
44
|
+
def get_messages #:nodoc:
|
45
|
+
raise NotImplementedError, "This method should be overridden by subclass"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Stub. Should be overridden by subclass.
|
49
|
+
def close_connection #:nodoc:
|
50
|
+
raise NotImplementedError, "This method should be overridden by subclass"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Send message to receiver object
|
54
|
+
def process_message(message)
|
55
|
+
@receiver.receive(message)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Stub. Should be overridden by subclass.
|
59
|
+
def handle_bogus_message(message) #:nodoc:
|
60
|
+
raise NotImplementedError, "This method should be overridden by subclass"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/fetcher/imap.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../vendor/plain_imap'
|
2
|
+
|
3
|
+
module Fetcher
|
4
|
+
class Imap < Base
|
5
|
+
|
6
|
+
PORT = 143
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Additional Options:
|
11
|
+
# * <tt>:authentication</tt> - authentication type to use, defaults to PLAIN
|
12
|
+
# * <tt>:port</tt> - port to use (defaults to 143)
|
13
|
+
# * <tt>:ssl</tt> - use SSL to connect
|
14
|
+
# * <tt>:use_login</tt> - use LOGIN instead of AUTHENTICATE to connect (some IMAP servers, like GMail, do not support AUTHENTICATE)
|
15
|
+
# * <tt>:processed_folder</tt> - if set to the name of a mailbox, messages will be moved to that mailbox instead of deleted after processing. The mailbox will be created if it does not exist.
|
16
|
+
# * <tt>:error_folder:</tt> - the name of a mailbox where messages that cannot be processed (i.e., your receiver throws an exception) will be moved. Defaults to "bogus". The mailbox will be created if it does not exist.
|
17
|
+
def initialize(options={})
|
18
|
+
@authentication = options.delete(:authentication) || 'PLAIN'
|
19
|
+
@port = options.delete(:port) || PORT
|
20
|
+
@ssl = options.delete(:ssl)
|
21
|
+
@use_login = options.delete(:use_login)
|
22
|
+
@processed_folder = options.delete(:processed_folder)
|
23
|
+
@error_folder = options.delete(:error_folder) || 'bogus'
|
24
|
+
super(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Open connection and login to server
|
28
|
+
def establish_connection
|
29
|
+
@connection = Net::IMAP.new(@server, @port, @ssl)
|
30
|
+
if @use_login
|
31
|
+
@connection.login(@username, @password)
|
32
|
+
else
|
33
|
+
@connection.authenticate(@authentication, @username, @password)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Retrieve messages from server
|
38
|
+
def get_messages
|
39
|
+
@connection.select('INBOX')
|
40
|
+
@connection.uid_search(['ALL']).each do |uid|
|
41
|
+
msg = @connection.uid_fetch(uid,'RFC822').first.attr['RFC822']
|
42
|
+
begin
|
43
|
+
process_message(msg)
|
44
|
+
add_to_processed_folder(uid) if @processed_folder
|
45
|
+
rescue
|
46
|
+
handle_bogus_message(msg)
|
47
|
+
end
|
48
|
+
# Mark message as deleted
|
49
|
+
@connection.uid_store(uid, "+FLAGS", [:Seen, :Deleted])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Store the message for inspection if the receiver errors
|
54
|
+
def handle_bogus_message(message)
|
55
|
+
create_mailbox(@error_folder)
|
56
|
+
@connection.append(@error_folder, message)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Delete messages and log out
|
60
|
+
def close_connection
|
61
|
+
@connection.expunge
|
62
|
+
@connection.logout
|
63
|
+
@connection.disconnect
|
64
|
+
rescue
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_to_processed_folder(uid)
|
68
|
+
create_mailbox(@processed_folder)
|
69
|
+
@connection.uid_copy(uid, @processed_folder)
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_mailbox(mailbox)
|
73
|
+
unless @connection.list("", mailbox)
|
74
|
+
@connection.create(mailbox)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/fetcher/pop.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../vendor/secure_pop'
|
2
|
+
|
3
|
+
module Fetcher
|
4
|
+
class Pop < Base
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
# Additional Options:
|
9
|
+
# * <tt>:ssl</tt> - whether or not to use ssl encryption
|
10
|
+
# * <tt>:port</tt> - port to use (defaults to 110)
|
11
|
+
def initialize(options={})
|
12
|
+
@ssl = options.delete(:ssl)
|
13
|
+
@port = options.delete(:port) || Net::POP3.default_port
|
14
|
+
super(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Open connection and login to server
|
18
|
+
def establish_connection
|
19
|
+
@connection = Net::POP3.new(@server, @port)
|
20
|
+
@connection.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if @ssl
|
21
|
+
@connection.start(@username, @password)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve messages from server
|
25
|
+
def get_messages
|
26
|
+
unless @connection.mails.empty?
|
27
|
+
@connection.each_mail do |msg|
|
28
|
+
begin
|
29
|
+
process_message(msg.pop)
|
30
|
+
rescue
|
31
|
+
handle_bogus_message(msg.pop)
|
32
|
+
end
|
33
|
+
# Delete message from server
|
34
|
+
msg.delete
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Store the message for inspection if the receiver errors
|
40
|
+
def handle_bogus_message(message)
|
41
|
+
# This needs a good solution
|
42
|
+
end
|
43
|
+
|
44
|
+
# Close connection to server
|
45
|
+
def close_connection
|
46
|
+
@connection.finish
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
# add plain as an authentication type...
|
3
|
+
# This is taken from:
|
4
|
+
# http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/lib/net/imap.rb?revision=7657&view=markup&pathrev=10966
|
5
|
+
|
6
|
+
# Authenticator for the "PLAIN" authentication type. See
|
7
|
+
# #authenticate().
|
8
|
+
class PlainAuthenticator
|
9
|
+
def process(data)
|
10
|
+
return "\0#{@user}\0#{@password}"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(user, password)
|
16
|
+
@user = user
|
17
|
+
@password = password
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Net::IMAP.add_authenticator "PLAIN", PlainAuthenticator
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'net/pop'
|
3
|
+
require 'net/protocol'
|
4
|
+
require 'openssl/ssl'
|
5
|
+
|
6
|
+
# Backport of ruby 1.9's POP3 SSL support
|
7
|
+
class Net::POP3
|
8
|
+
@@usessl = nil
|
9
|
+
@@verify = nil
|
10
|
+
@@certs = nil
|
11
|
+
PORT = 110
|
12
|
+
SSL_PORT = 995
|
13
|
+
|
14
|
+
def self.default_port
|
15
|
+
PORT
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default_ssl_port
|
19
|
+
SSL_PORT
|
20
|
+
end
|
21
|
+
|
22
|
+
# Enable SSL for all new instances.
|
23
|
+
# +verify+ is the type of verification to do on the Server Cert; Defaults
|
24
|
+
# to OpenSSL::SSL::VERIFY_PEER.
|
25
|
+
# +certs+ is a file or directory holding CA certs to use to verify the
|
26
|
+
# server cert; Defaults to nil.
|
27
|
+
def self.enable_ssl( verify = OpenSSL::SSL::VERIFY_PEER, certs = nil )
|
28
|
+
@@usessl = true
|
29
|
+
@@verify = verify
|
30
|
+
@@certs = certs
|
31
|
+
end
|
32
|
+
|
33
|
+
# Disable SSL for all new instances.
|
34
|
+
def self.disable_ssl
|
35
|
+
@@usessl = nil
|
36
|
+
@@verify = nil
|
37
|
+
@@certs = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Creates a new POP3 object.
|
41
|
+
# +addr+ is the hostname or ip address of your POP3 server.
|
42
|
+
# The optional +port+ is the port to connect to.
|
43
|
+
# The optional +isapop+ specifies whether this connection is going
|
44
|
+
# to use APOP authentication; it defaults to +false+.
|
45
|
+
# This method does *not* open the TCP connection.
|
46
|
+
def initialize(addr, port = nil, isapop = false)
|
47
|
+
@address = addr
|
48
|
+
@usessl = @@usessl
|
49
|
+
if @usessl
|
50
|
+
@port = port || SSL_PORT
|
51
|
+
else
|
52
|
+
@port = port || PORT
|
53
|
+
end
|
54
|
+
@apop = isapop
|
55
|
+
|
56
|
+
@certs = @@certs
|
57
|
+
@verify = @@verify
|
58
|
+
|
59
|
+
@command = nil
|
60
|
+
@socket = nil
|
61
|
+
@started = false
|
62
|
+
@open_timeout = 30
|
63
|
+
@read_timeout = 60
|
64
|
+
@debug_output = nil
|
65
|
+
|
66
|
+
@mails = nil
|
67
|
+
@n_mails = nil
|
68
|
+
@n_bytes = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# does this instance use SSL?
|
72
|
+
def usessl?
|
73
|
+
@usessl
|
74
|
+
end
|
75
|
+
|
76
|
+
# Enables SSL for this instance. Must be called before the connection is
|
77
|
+
# established to have any effect.
|
78
|
+
# +verify+ is the type of verification to do on the Server Cert; Defaults
|
79
|
+
# to OpenSSL::SSL::VERIFY_PEER.
|
80
|
+
# +certs+ is a file or directory holding CA certs to use to verify the
|
81
|
+
# server cert; Defaults to nil.
|
82
|
+
# +port+ is port to establish the SSL conection on; Defaults to 995.
|
83
|
+
def enable_ssl(verify = OpenSSL::SSL::VERIFY_PEER, certs = nil,
|
84
|
+
port = SSL_PORT)
|
85
|
+
@usessl = true
|
86
|
+
@verify = verify
|
87
|
+
@certs = certs
|
88
|
+
@port = port
|
89
|
+
end
|
90
|
+
|
91
|
+
def disable_ssl
|
92
|
+
@usessl = nil
|
93
|
+
@verify = nil
|
94
|
+
@certs = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def do_start( account, password )
|
98
|
+
s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
99
|
+
if @usessl
|
100
|
+
unless defined?(OpenSSL)
|
101
|
+
raise "SSL extension not installed"
|
102
|
+
end
|
103
|
+
sslctx = OpenSSL::SSL::SSLContext.new
|
104
|
+
sslctx.verify_mode = @verify
|
105
|
+
sslctx.ca_file = @certs if @certs && FileTest::file?(@certs)
|
106
|
+
sslctx.ca_path = @certs if @certs && FileTest::directory?(@certs)
|
107
|
+
s = OpenSSL::SSL::SSLSocket.new(s, sslctx)
|
108
|
+
s.sync_close = true
|
109
|
+
s.connect
|
110
|
+
end
|
111
|
+
|
112
|
+
@socket = Net::InternetMessageIO.new(s)
|
113
|
+
on_connect
|
114
|
+
@command = Net::POP3Command.new(@socket)
|
115
|
+
if apop?
|
116
|
+
@command.apop account, password
|
117
|
+
else
|
118
|
+
@command.auth account, password
|
119
|
+
end
|
120
|
+
@started = true
|
121
|
+
ensure
|
122
|
+
do_finish if not @started
|
123
|
+
end
|
124
|
+
private :do_start
|
125
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# require File.dirname(__FILE__) + '/../../../../config/boot'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'test/unit'
|
4
|
+
require 'mocha'
|
5
|
+
require 'fetcher'
|
6
|
+
|
7
|
+
class FetcherTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@receiver = mock()
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_set_configuration_instance_variables
|
14
|
+
create_fetcher
|
15
|
+
assert_equal 'test.host', @fetcher.instance_variable_get(:@server)
|
16
|
+
assert_equal 'name', @fetcher.instance_variable_get(:@username)
|
17
|
+
assert_equal 'password', @fetcher.instance_variable_get(:@password)
|
18
|
+
assert_equal @receiver, @fetcher.instance_variable_get(:@receiver)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_require_subclass
|
22
|
+
create_fetcher
|
23
|
+
assert_raise(NotImplementedError) { @fetcher.fetch }
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_should_require_server
|
27
|
+
assert_raise(ArgumentError) { create_fetcher(:server => nil) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_require_username
|
31
|
+
assert_raise(ArgumentError) { create_fetcher(:username => nil) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_should_require_password
|
35
|
+
assert_raise(ArgumentError) { create_fetcher(:password => nil) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_should_require_receiver
|
39
|
+
assert_raise(ArgumentError) { create_fetcher(:receiver => nil) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_fetcher(options={})
|
43
|
+
@fetcher = Fetcher::Base.new({:server => 'test.host', :username => 'name', :password => 'password', :receiver => @receiver}.merge(options))
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
class FactoryFetcherTest < Test::Unit::TestCase
|
49
|
+
|
50
|
+
def setup
|
51
|
+
@receiver = mock()
|
52
|
+
@pop_fetcher = Fetcher.create(:type => :pop, :server => 'test.host',
|
53
|
+
:username => 'name',
|
54
|
+
:password => 'password',
|
55
|
+
:receiver => @receiver)
|
56
|
+
|
57
|
+
@imap_fetcher = Fetcher.create(:type => :imap, :server => 'test.host',
|
58
|
+
:username => 'name',
|
59
|
+
:password => 'password',
|
60
|
+
:receiver => @receiver)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_should_be_sublcass
|
64
|
+
assert_equal Fetcher::Pop, @pop_fetcher.class
|
65
|
+
assert_equal Fetcher::Imap, @imap_fetcher.class
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_should_require_type
|
69
|
+
assert_raise(ArgumentError) { Fetcher.create({}) }
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# Write tests for sub-classes
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: winton-fetcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luke Francl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-27 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: look@recursion.org
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- fetcher.gemspec
|
26
|
+
- gemspec.rb
|
27
|
+
- init.rb
|
28
|
+
- lib
|
29
|
+
- lib/fetcher
|
30
|
+
- lib/fetcher/base.rb
|
31
|
+
- lib/fetcher/imap.rb
|
32
|
+
- lib/fetcher/pop.rb
|
33
|
+
- lib/fetcher.rb
|
34
|
+
- lib/vendor
|
35
|
+
- lib/vendor/plain_imap.rb
|
36
|
+
- lib/vendor/secure_pop.rb
|
37
|
+
- MIT-LICENSE
|
38
|
+
- Rakefile
|
39
|
+
- README.rdoc
|
40
|
+
- test
|
41
|
+
- test/fetcher_test.rb
|
42
|
+
has_rdoc: false
|
43
|
+
homepage: http://github.com/winton/fetcher
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.2.0
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: download email from POP3 or IMAP and do stuff with it. gemified fork
|
68
|
+
test_files: []
|
69
|
+
|