smailer 0.4.4 → 0.5.0

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/lib/smailer.rb CHANGED
@@ -1,5 +1,7 @@
1
+ require 'smailer/constants'
2
+ require 'smailer/version'
3
+
1
4
  module Smailer
2
- autoload :VERSION, 'smailer/version'
3
5
  autoload :Models, 'smailer/models'
4
6
  autoload :Tasks, 'smailer/tasks'
5
7
  autoload :Compatibility, 'smailer/compatibility'
@@ -0,0 +1,3 @@
1
+ module Smailer
2
+ BOUNCES_PREFIX = 'bounces-'.freeze
3
+ end
data/lib/smailer/tasks.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Smailer
2
2
  module Tasks
3
3
  autoload :Send, 'smailer/tasks/send'
4
+ autoload :ProcessBounces, 'smailer/tasks/process_bounces'
4
5
  end
5
6
  end
@@ -0,0 +1,162 @@
1
+ require 'bounce_email'
2
+ require 'net/pop'
3
+
4
+ module Smailer
5
+ module Tasks
6
+ class ProcessBounces
7
+ @@keys_messages = {}
8
+ @@bounce_counts = Hash.new(0)
9
+ @@emails_bounces = {}
10
+ @@unsubscribed = {}
11
+ @@test_mode = false
12
+
13
+ class << self
14
+ # You need to provide at least a :server, :username and a :password options to execute().
15
+ # These will represent the POP3 connection details to the bounce mailbox which will be processed.
16
+ # Also consider providing a concrete implementation to the :subscribed_checker option, so that
17
+ # bounces for unknown or already-unsubscribed emails do not remain clogging-up the mailbox
18
+ # Example usage:
19
+ #
20
+ # Smailer::Tasks::ProcessBounces.execute({
21
+ # :server => 'bounces.example.org',
22
+ # :username => 'noreply@bounces.example.org',
23
+ # :password => 'somesecret',
24
+ # :subscribed_checker => lambda { |recipient| Subscribers.subscribed.where(:email => recipient).first.present? },
25
+ # }) do |unsubscribe_details|
26
+ # Subscribers.where(:email => unsubscribe_details[:recipient]).destroy
27
+ # end
28
+ def execute(options = {})
29
+ deleted = 0
30
+ options = {
31
+ :port => 110,
32
+ :subscribed_checker => lambda { true },
33
+ :bounce_counts_per_type_to_unsubscribe => {
34
+ 1 => ['5.1.1', '5.1.2', '5.1.3', '5.1.6', '5.1.8', '5.7.1'],
35
+ 3 => ['5.2.1', '5.1.0'],
36
+ 5 => ['5.5.0'],
37
+ 8 => ['5.0.0'],
38
+ 12 => ['5.2.2', '5.2.0', '5.3.0', '5.3.2'],
39
+ 16 => ['4.2.2', '4.4.2', '4.0.0'],
40
+ 30 => ['97', '98', '99'],
41
+ },
42
+ :default_bounce_count_per_type_to_unsubscribe => 20,
43
+ :total_bounce_count_to_unsubscribe => 40,
44
+ :delete_unprocessed_bounces => true,
45
+ :logger => lambda { |msg| puts msg },
46
+ }.merge(options)
47
+
48
+ logger = lambda do |msg|
49
+ options[:logger].call msg if options[:logger] && options[:logger].respond_to?(:call)
50
+ end
51
+
52
+ connect_to_mailbox(options) do |pop|
53
+ mails_to_process = pop.mails.size
54
+ logger.call "#{mails_to_process} mail(s) to process"
55
+
56
+ pop.mails.each_with_index do |m, i|
57
+ logger.call "#{((i + 1) * 100.0 / mails_to_process).round(2)}% (#{i + 1} of #{mails_to_process})" if i % 200 == 0
58
+
59
+ mail = Mail.new m.pop
60
+ bounce = BounceEmail::Mail.new mail
61
+ processed = false
62
+
63
+ if bounce.bounced?
64
+ original_email = sent_message_from_bounce(mail)
65
+
66
+ if original_email
67
+ recipient = original_email.to
68
+
69
+ if !unsubscribed?(recipient) && options[:subscribed_checker].call(recipient)
70
+ processed = true
71
+ register_bounce bounce, m
72
+
73
+ bounce_count_per_type = bounce_count_for recipient, bounce.code
74
+ total_bounce_count = bounce_count_for recipient
75
+
76
+ rule = options[:bounce_counts_per_type_to_unsubscribe].select { |count, codes| codes.include?(bounce.code) }.first
77
+ max_count_per_type = rule ? rule.first : options[:default_bounce_count_per_type_to_unsubscribe]
78
+
79
+ bounce_count_per_type_exceeded = bounce_count_per_type >= max_count_per_type
80
+ total_bounce_count_exceeded = total_bounce_count >= options[:total_bounce_count_to_unsubscribe]
81
+
82
+ if bounce_count_per_type_exceeded || total_bounce_count_exceeded
83
+ logger.call "=> Unsubscribing #{recipient} and deleting #{bounces_for(recipient).size} bounced email(s)"
84
+
85
+ yield :recipient => recipient, :original_email => original_email, :bounce => bounce
86
+ unsubscribe recipient
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ if options[:delete_unprocessed_bounces] && !processed
93
+ m.delete unless test_mode?
94
+ deleted += 1
95
+ end
96
+ end
97
+ end
98
+
99
+ logger.call "Deleted #{deleted} unprocessed bounce(s) from the mailbox" if deleted > 0
100
+ connect_to_mailbox(options) { |pop| logger.call "Messages left in the mailbox: #{pop.mails.size}" }
101
+ end
102
+
103
+ def test_mode?
104
+ @@test_mode
105
+ end
106
+
107
+ def test_mode=(mode)
108
+ @@test_mode = mode
109
+ end
110
+
111
+ private
112
+
113
+ def connect_to_mailbox(options, &block)
114
+ Net::POP3.start(options[:server], options[:port], options[:username], options[:password], &block)
115
+ end
116
+
117
+ def unsubscribed?(recipient)
118
+ @@unsubscribed[recipient]
119
+ end
120
+
121
+ def unsubscribe(recipient)
122
+ @@unsubscribed[recipient] = true
123
+
124
+ bounces_for(recipient).each do |pop_mail|
125
+ pop_mail.delete unless test_mode?
126
+ end
127
+
128
+ @@emails_bounces[recipient] = nil
129
+ end
130
+
131
+ def register_bounce(bounce, pop_mail)
132
+ increment_bounce_count_for recipient, bounce.code
133
+ bounces_for(recipient) << pop_mail
134
+ end
135
+
136
+ def bounces_for(recipient)
137
+ @@emails_bounces[recipient] ||= []
138
+ end
139
+
140
+ def bounce_count_for(email, error_code = nil)
141
+ key = [email, error_code]
142
+ @@bounce_counts[key]
143
+ end
144
+
145
+ def increment_bounce_count_for(email, error_code = nil)
146
+ [error_code, nil].uniq.each do |code|
147
+ @@bounce_counts[[email, code]] += 1
148
+ end
149
+ end
150
+
151
+ def sent_message_from_bounce(mail)
152
+ to = mail.to.select { |address| address.to_s.start_with? Smailer::BOUNCES_PREFIX }.first
153
+ return nil if to.nil?
154
+
155
+ key = to.strip.split('@').first[Smailer::BOUNCES_PREFIX.size..-1]
156
+
157
+ @@keys_messages[key] ||= Smailer::Models::FinishedMail.where(:key => key).first
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -55,7 +55,7 @@ module Smailer
55
55
  # compute the VERP'd return_path if requested
56
56
  # or fall-back to a global return-path if not
57
57
  item_return_path = if options[:verp]
58
- "bounces-#{queue_item.key}@#{options[:return_path_domain]}"
58
+ "#{Smailer::BOUNCES_PREFIX}#{queue_item.key}@#{options[:return_path_domain]}"
59
59
  else
60
60
  options[:return_path]
61
61
  end
@@ -1,7 +1,7 @@
1
1
  module Smailer
2
2
  MAJOR = 0
3
- MINOR = 4
4
- PATCH = 4
3
+ MINOR = 5
4
+ PATCH = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
data/smailer.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
16
16
 
17
17
  s.add_development_dependency "bundler", ">= 1.0.0"
18
18
  s.add_runtime_dependency "mail", "~> 2.2"
19
+ s.add_runtime_dependency "bounce_email", "~> 0.2"
19
20
 
20
21
  s.files = `git ls-files`.split("\n")
21
22
  s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smailer
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
9
- - 4
10
- version: 0.4.4
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Dimitar Dimitrov
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-15 00:00:00 +02:00
18
+ date: 2011-10-27 00:00:00 +03:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -49,6 +49,21 @@ dependencies:
49
49
  version: "2.2"
50
50
  type: :runtime
51
51
  version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: bounce_email
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 15
61
+ segments:
62
+ - 0
63
+ - 2
64
+ version: "0.2"
65
+ type: :runtime
66
+ version_requirements: *id003
52
67
  description: A simple mailer for newsletters with basic campaign management, queue and unsubscribe support. To be used within Rails.
53
68
  email:
54
69
  - wireman@gmail.com
@@ -70,6 +85,7 @@ files:
70
85
  - lib/generators/smailer/templates/migration.rb.erb
71
86
  - lib/smailer.rb
72
87
  - lib/smailer/compatibility.rb
88
+ - lib/smailer/constants.rb
73
89
  - lib/smailer/models.rb
74
90
  - lib/smailer/models/finished_mail.rb
75
91
  - lib/smailer/models/mail_campaign.rb
@@ -78,6 +94,7 @@ files:
78
94
  - lib/smailer/models/property.rb
79
95
  - lib/smailer/models/queued_mail.rb
80
96
  - lib/smailer/tasks.rb
97
+ - lib/smailer/tasks/process_bounces.rb
81
98
  - lib/smailer/tasks/send.rb
82
99
  - lib/smailer/version.rb
83
100
  - rails/init.rb