bounce_fetcher 0.0.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/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in bounce_fetcher.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Michael Guterl
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.md ADDED
@@ -0,0 +1,27 @@
1
+ # Bounce Fetcher
2
+
3
+ A set of tools for processing email bounces in a flexible way.
4
+
5
+ ## Usage
6
+
7
+ bounces = BounceFetcher.pop3('pop3.host.com', 'username', 'password')
8
+ bounces.each do |email|
9
+ # do something application specific
10
+ # For example: User.bounce_email(email)
11
+ end
12
+
13
+ This will fetch each email from the POP3 server specified, if the email is a determined to be a permanent bounce, we will attempt to determine what email address the bounce is for, yield it to the block, and the email will be deleted from the POP3 server.
14
+
15
+ ## Note on Patches/Pull Requests
16
+
17
+ * Fork the project.
18
+ * Make your feature addition or bug fix.
19
+ * Add tests for it. This is important so I don't break it in a
20
+ future version unintentionally.
21
+ * Commit, do not mess with rakefile, version, or history.
22
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
23
+ * Send me a pull request. Bonus points for topic branches.
24
+
25
+ ## Copyright
26
+
27
+ Copyright (c) 2011 Michael Guterl. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/test_*.rb"
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "bounce_fetcher/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "bounce_fetcher"
7
+ s.version = BounceFetcher::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Michael Guterl"]
10
+ s.email = ["michael@diminishing.org"]
11
+ s.homepage = "http://github.com/mguterl/bounce_fetcher"
12
+ s.summary = %q{A set of tools for processing email bounces in a flexible way.}
13
+ s.description = %q{A set of tools for processing email bounces in a flexible way.}
14
+
15
+ s.rubyforge_project = "bounce_fetcher"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency 'tmail'
23
+ s.add_dependency 'bounce-email', '0.0.1'
24
+ end
@@ -0,0 +1,39 @@
1
+ module BounceFetcher
2
+
3
+ def self.new(*args, &block)
4
+ BounceFetcher.new(*args, &block)
5
+ end
6
+
7
+ def self.pop3(host, username, password)
8
+ fetcher = PopFetcher.new(host, username, password)
9
+ extractor = Extractor.new
10
+ detector = Detector.new
11
+ new(fetcher, extractor, detector)
12
+ end
13
+
14
+ class BounceFetcher
15
+
16
+ def initialize(fetcher, extractor, detector)
17
+ @fetcher = fetcher
18
+ @extractor = extractor
19
+ @detector = detector
20
+ end
21
+
22
+ def each
23
+ @fetcher.each do |e|
24
+ if @detector.permanent_bounce?(e)
25
+ @extractor.extract_emails(e).each do |email|
26
+ yield email
27
+ end
28
+ e.delete
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ require 'bounce_fetcher/pop_fetcher'
38
+ require 'bounce_fetcher/email_extractor'
39
+ require 'bounce_fetcher/bounce_detector'
@@ -0,0 +1,14 @@
1
+ require 'bounce-email'
2
+
3
+ module BounceFetcher
4
+
5
+ class BounceDetector
6
+
7
+ def permanent_bounce?(email)
8
+ bounce = BounceEmail::Mail.new(email)
9
+ bounce.isbounce && bounce.type == "Permanent Failure"
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,13 @@
1
+ module BounceFetcher
2
+
3
+ class EmailExtractor
4
+
5
+ EMAIL_REGEX = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b/i
6
+
7
+ def extract_emails(string)
8
+ string.scan(EMAIL_REGEX).compact.uniq
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,54 @@
1
+ # lifted from the action_mailer_verp plugin:
2
+ # http://github.com/jamesgolick/action_mailer_verp
3
+
4
+ module BounceProcessor
5
+
6
+ class PopFetcher
7
+
8
+ class Email
9
+ def initialize(email)
10
+ @email = email
11
+ end
12
+
13
+ def delete
14
+ @email.delete
15
+ end
16
+
17
+ def method_missing(meth, *args, &block)
18
+ parsed_email.send(meth, *args, &block)
19
+ end
20
+
21
+ private
22
+
23
+ def parsed_email
24
+ @parsed_email ||= TMail::Mail.parse(@email.pop)
25
+ end
26
+ end
27
+
28
+ def initialize(host, username, password)
29
+ @host = host
30
+ @username = username
31
+ @password = password
32
+ end
33
+
34
+ def each
35
+ connection.each_mail do |e|
36
+ yield Email.new(e)
37
+ end
38
+ connection.finish
39
+ end
40
+
41
+ private
42
+
43
+ def connection
44
+ if @connection.nil?
45
+ @connection = Net::POP3.new(@host)
46
+ @connection.start(@username, @password)
47
+ end
48
+
49
+ @connection
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,3 @@
1
+ module BounceFetcher
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,44 @@
1
+ require 'minitest/autorun'
2
+ require 'bounce_fetcher'
3
+
4
+ class TestBounceDetector < MiniTest::Unit::TestCase
5
+
6
+ class Email < Struct.new(:subject, :body)
7
+ def parts
8
+ []
9
+ end
10
+ end
11
+
12
+ class EmailWithBounceStatus
13
+ class Part < Struct.new(:body); end
14
+
15
+ def subject
16
+ "mail delivery failed"
17
+ end
18
+
19
+ def parts
20
+ [nil, Part.new("Status: 5.2.0")]
21
+ end
22
+ end
23
+
24
+ def setup
25
+ @detector = BounceFetcher::BounceDetector.new
26
+ end
27
+
28
+ def test_permanent_bounce_with_temporary_failure
29
+ assert !@detector.permanent_bounce?(Email.new("mail delivery failed"))
30
+ end
31
+
32
+ def test_permanent_bounce_with_out_of_town_auto_responder
33
+ assert !@detector.permanent_bounce?(Email.new("Out of Town"))
34
+ end
35
+
36
+ def test_permanent_bounce_with_a_permanent_bounce_body
37
+ assert @detector.permanent_bounce?(Email.new("mail delivery failed", "mailbox unavailable"))
38
+ end
39
+
40
+ def test_permanent_bounce_with_a_permanent_bounce_status
41
+ assert @detector.permanent_bounce?(EmailWithBounceStatus.new)
42
+ end
43
+
44
+ end
@@ -0,0 +1,56 @@
1
+ require 'minitest/autorun'
2
+ require 'bounce_fetcher'
3
+
4
+ class TestBounceFetcher < MiniTest::Unit::TestCase
5
+
6
+ class FakePopFetcher
7
+ def initialize(*emails)
8
+ @emails = emails
9
+ end
10
+
11
+ def each
12
+ @emails.each { |email| yield email }
13
+ end
14
+ end
15
+
16
+ class Email < Struct.new(:body, :permanent_bounce)
17
+ def delete
18
+ @deleted = true
19
+ end
20
+
21
+ def deleted?
22
+ @deleted
23
+ end
24
+ end
25
+
26
+ class FakeExtractor
27
+ def extract_emails(tmail)
28
+ [tmail.body]
29
+ end
30
+ end
31
+
32
+ class FakeBounceDetector
33
+ def permanent_bounce?(tmail)
34
+ tmail.permanent_bounce
35
+ end
36
+ end
37
+
38
+ def test_each_only_returns_emails_for_permanent_bounces
39
+ permanent_bounce = Email.new('foo@bar.com', true)
40
+ temporary_bounce = Email.new('bar@baz.com', false)
41
+ pop_fetcher = FakePopFetcher.new(permanent_bounce, temporary_bounce)
42
+ extractor = FakeExtractor.new
43
+ detector = FakeBounceDetector.new
44
+ fetcher = BounceFetcher.new(pop_fetcher, extractor, detector)
45
+
46
+ emails = []
47
+ fetcher.each do |email|
48
+ emails << email
49
+ end
50
+
51
+ assert_equal ['foo@bar.com'], emails
52
+ assert permanent_bounce.deleted?
53
+ assert !temporary_bounce.deleted?
54
+ end
55
+
56
+ end
@@ -0,0 +1,18 @@
1
+ require 'minitest/autorun'
2
+ require 'bounce_fetcher'
3
+
4
+ class TestEmailExtractor < MiniTest::Unit::TestCase
5
+
6
+ def setup
7
+ @extractor = BounceFetcher::EmailExtractor.new
8
+ end
9
+
10
+ def test_extract_emails
11
+ assert_equal ['foo@bar.com', 'bar@baz.com'], @extractor.extract_emails("foo@bar.com\nbar@baz.com")
12
+ end
13
+
14
+ def test_extract_emails_ignores_duplicates
15
+ assert_equal ['foo@bar.com'], @extractor.extract_emails("foo@bar.com\nfoo@bar.com")
16
+ end
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bounce_fetcher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Guterl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-04-06 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: tmail
17
+ requirement: &2162141280 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2162141280
26
+ - !ruby/object:Gem::Dependency
27
+ name: bounce-email
28
+ requirement: &2162140740 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - =
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2162140740
37
+ description: A set of tools for processing email bounces in a flexible way.
38
+ email:
39
+ - michael@diminishing.org
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - LICENSE
47
+ - README.md
48
+ - Rakefile
49
+ - bounce_fetcher.gemspec
50
+ - lib/bounce_fetcher.rb
51
+ - lib/bounce_fetcher/bounce_detector.rb
52
+ - lib/bounce_fetcher/email_extractor.rb
53
+ - lib/bounce_fetcher/pop_fetcher.rb
54
+ - lib/bounce_fetcher/version.rb
55
+ - test/test_bounce_detector.rb
56
+ - test/test_bounce_fetcher.rb
57
+ - test/test_email_extractor.rb
58
+ has_rdoc: true
59
+ homepage: http://github.com/mguterl/bounce_fetcher
60
+ licenses: []
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project: bounce_fetcher
79
+ rubygems_version: 1.5.2
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: A set of tools for processing email bounces in a flexible way.
83
+ test_files:
84
+ - test/test_bounce_detector.rb
85
+ - test/test_bounce_fetcher.rb
86
+ - test/test_email_extractor.rb