VvanGemert-ar_mailer 2.1.8 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +6 -1
- data/Rakefile +1 -14
- data/lib/action_mailer/ar_mailer.rb +18 -0
- data/lib/action_mailer/ar_sendmail.rb +248 -6
- data/lib/adzap-ar_mailer.rb +1 -0
- metadata +6 -37
data/README.rdoc
CHANGED
@@ -16,4 +16,9 @@ http://github.com/adzap/ar_mailer/wikis
|
|
16
16
|
|
17
17
|
== Changes to the Adzap_ar_mailer version
|
18
18
|
|
19
|
-
- Added priority
|
19
|
+
- Added priority, the higher the priority the faster it will send
|
20
|
+
- Added newsletter support, loops through user table and is connected with the batch size
|
21
|
+
- Added bounce check support, updates user with bounced_at field when bounced
|
22
|
+
- Added dry run support, dry run doesn't send emails and doesn't check for bounced
|
23
|
+
|
24
|
+
Use "ar_sendmail -h" for all the options
|
data/Rakefile
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake/gempackagetask'
|
3
|
-
require 'rake/testtask'
|
4
3
|
require 'rake/rdoctask'
|
5
4
|
|
6
5
|
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/lib'))
|
@@ -44,9 +43,7 @@ ar_mailer_gemspec = Gem::Specification.new do |s|
|
|
44
43
|
s.require_paths = ["lib"]
|
45
44
|
s.rubyforge_project = %q{seattlerb}
|
46
45
|
s.summary = %q{A two-phase delivery agent for ActionMailer}
|
47
|
-
s.test_files = [
|
48
|
-
s.add_development_dependency "minitest", ">= 1.5.0"
|
49
|
-
s.add_development_dependency "mocha", ">= 0.9.8"
|
46
|
+
s.test_files = []
|
50
47
|
end
|
51
48
|
|
52
49
|
Rake::GemPackageTask.new(ar_mailer_gemspec) do |pkg|
|
@@ -64,13 +61,3 @@ desc "Build packages and install"
|
|
64
61
|
task :install => :package do
|
65
62
|
sh %{sudo gem install --local --test pkg/VvanGemert-ar_mailer-#{ActionMailer::ARSendmail::VERSION}}
|
66
63
|
end
|
67
|
-
|
68
|
-
desc 'Default: run unit tests.'
|
69
|
-
task :default => :test
|
70
|
-
|
71
|
-
desc 'Test the ar_mailer gem.'
|
72
|
-
Rake::TestTask.new(:test) do |t|
|
73
|
-
t.libs << 'lib' << 'test'
|
74
|
-
t.test_files = FileList['test/**/test_*.rb'].exclude("test/test_helper.rb")
|
75
|
-
t.verbose = true
|
76
|
-
end
|
@@ -9,6 +9,8 @@ class ActionMailer::Base
|
|
9
9
|
# Set the email class for deliveries. Handle class reloading issues which prevents caching the email class.
|
10
10
|
#
|
11
11
|
@@email_class_name = 'Email'
|
12
|
+
@@newsletter_class_name = 'EmailNewsletter'
|
13
|
+
@@user_class_name = 'User'
|
12
14
|
@@priority = 100
|
13
15
|
|
14
16
|
def self.email_class=(klass)
|
@@ -19,6 +21,22 @@ class ActionMailer::Base
|
|
19
21
|
@@email_class_name.constantize
|
20
22
|
end
|
21
23
|
|
24
|
+
def self.newsletter_class=(klass)
|
25
|
+
@@newsletter_class_name = klass.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.newsletter_class
|
29
|
+
@@newsletter_class_name.constantize
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.user_class=(klass)
|
33
|
+
@@user_class_name = klass.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.user_class
|
37
|
+
@@user_class_name.constantize
|
38
|
+
end
|
39
|
+
|
22
40
|
##
|
23
41
|
# Adds +mail+ to the Email table. Only the first From address for +mail+ is
|
24
42
|
# used.
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'net/smtp'
|
3
|
+
require 'net/imap'
|
3
4
|
require 'smtp_tls' unless Net::SMTP.instance_methods.include?("enable_starttls_auto")
|
4
5
|
|
5
6
|
##
|
@@ -146,6 +147,11 @@ class ActionMailer::ARSendmail
|
|
146
147
|
options[:MaxAge] = 86400 * 7
|
147
148
|
options[:Once] = false
|
148
149
|
options[:RailsEnv] = ENV['RAILS_ENV']
|
150
|
+
options[:Port] = 993
|
151
|
+
options[:Login] = ''
|
152
|
+
options[:Imap] = ''
|
153
|
+
options[:Password] = ''
|
154
|
+
options[:DryRun] = false
|
149
155
|
options[:Pidfile] = options[:Chdir] + '/log/ar_sendmail.pid'
|
150
156
|
|
151
157
|
opts = OptionParser.new do |opts|
|
@@ -229,7 +235,43 @@ class ActionMailer::ARSendmail
|
|
229
235
|
"Default: #{options[:Verbose]}") do |verbose|
|
230
236
|
options[:Verbose] = verbose
|
231
237
|
end
|
232
|
-
|
238
|
+
|
239
|
+
opts.on("-i", "--imap IMAP",
|
240
|
+
"Imap server used to check for bounces",
|
241
|
+
"Default: false", String) do |imap|
|
242
|
+
options[:Imap] = imap
|
243
|
+
end
|
244
|
+
|
245
|
+
opts.on("-l", "--login LOGIN",
|
246
|
+
"login name to check for bounces",
|
247
|
+
"Default: false", String) do |login|
|
248
|
+
options[:Login] = login
|
249
|
+
end
|
250
|
+
|
251
|
+
opts.on( "--password PASSWORD",
|
252
|
+
"password name to check for bounces",
|
253
|
+
"Default: false", String) do |password|
|
254
|
+
options[:Password] = password
|
255
|
+
end
|
256
|
+
|
257
|
+
opts.on( "--port PORT",
|
258
|
+
"port to check for bounces",
|
259
|
+
"Default: #{options[:Port]}", Integer) do |port|
|
260
|
+
options[:Port] = port
|
261
|
+
end
|
262
|
+
|
263
|
+
opts.on( "-k", "--bouncecheck",
|
264
|
+
"check for bounces",
|
265
|
+
"Default: false") do |bounce_check|
|
266
|
+
options[:Bouncecheck] = true
|
267
|
+
end
|
268
|
+
|
269
|
+
opts.on("-f", "--dry-run",
|
270
|
+
"Dry run: don't send any emails",
|
271
|
+
"Default: Deliver all available emails\n", options[:DryRun]) do |dry_run|
|
272
|
+
options[:DryRun] = dry_run
|
273
|
+
end
|
274
|
+
|
233
275
|
opts.on("-h", "--help",
|
234
276
|
"You're looking at it") do
|
235
277
|
usage opts
|
@@ -335,7 +377,9 @@ class ActionMailer::ARSendmail
|
|
335
377
|
@once = options[:Once]
|
336
378
|
@verbose = options[:Verbose]
|
337
379
|
@max_age = options[:MaxAge]
|
338
|
-
|
380
|
+
@dry_run = options[:DryRun]
|
381
|
+
@imap = { :host => options[:Imap], :port => options[:Port], :user => options[:Login], :password => options[:Password] }
|
382
|
+
@bouncecheck = options[:Bouncecheck]
|
339
383
|
@failed_auth_count = 0
|
340
384
|
end
|
341
385
|
|
@@ -375,8 +419,12 @@ class ActionMailer::ARSendmail
|
|
375
419
|
until emails.empty? do
|
376
420
|
email = emails.shift
|
377
421
|
begin
|
378
|
-
|
379
|
-
|
422
|
+
if @dry_run
|
423
|
+
res = 'DRY RUN'
|
424
|
+
else
|
425
|
+
res = session.send_message email.mail, email.from, email.to
|
426
|
+
email.destroy
|
427
|
+
end
|
380
428
|
log "sent email %011d from %s to %s: %p" %
|
381
429
|
[email.id, email.from, email.to, res]
|
382
430
|
rescue Net::SMTPFatalError => e
|
@@ -389,7 +437,7 @@ class ActionMailer::ARSendmail
|
|
389
437
|
return
|
390
438
|
rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError, Timeout::Error => e
|
391
439
|
email.last_send_attempt = Time.now.to_i
|
392
|
-
email.save rescue nil
|
440
|
+
email.save rescue nil unless @dry_run
|
393
441
|
log "error sending email %d: %p(%s):\n\t%s" %
|
394
442
|
[email.id, e.message, e.class, e.backtrace.join("\n\t")]
|
395
443
|
session.reset
|
@@ -409,6 +457,61 @@ class ActionMailer::ARSendmail
|
|
409
457
|
# ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
|
410
458
|
end
|
411
459
|
|
460
|
+
def deliver_newsletter(newsletter, users)
|
461
|
+
|
462
|
+
settings = [
|
463
|
+
smtp_settings[:domain],
|
464
|
+
(smtp_settings[:user] || smtp_settings[:user_name]),
|
465
|
+
smtp_settings[:password],
|
466
|
+
smtp_settings[:authentication]
|
467
|
+
]
|
468
|
+
|
469
|
+
smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
|
470
|
+
if smtp.respond_to?(:enable_starttls_auto)
|
471
|
+
smtp.enable_starttls_auto unless smtp_settings[:tls] == false
|
472
|
+
else
|
473
|
+
settings << smtp_settings[:tls]
|
474
|
+
end
|
475
|
+
|
476
|
+
smtp.start(*settings) do |session|
|
477
|
+
@failed_auth_count = 0
|
478
|
+
until users.empty? do
|
479
|
+
user = users.shift
|
480
|
+
begin
|
481
|
+
if @dry_run
|
482
|
+
res = 'DRY RUN'
|
483
|
+
else
|
484
|
+
res = session.send_message newsletter.mail, newsletter.from, user.email
|
485
|
+
end
|
486
|
+
log "sent email %011d from %s to %s: %p" %
|
487
|
+
[newsletter.id, newsletter.from, user.email, res]
|
488
|
+
rescue Net::SMTPFatalError => e
|
489
|
+
log "5xx error sending email %d, removing from queue: %p(%s):\n\t%s" %
|
490
|
+
[email.id, e.message, e.class, e.backtrace.join("\n\t")]
|
491
|
+
session.reset
|
492
|
+
rescue Net::SMTPServerBusy => e
|
493
|
+
log "server too busy, stopping delivery cycle"
|
494
|
+
return
|
495
|
+
rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError, Timeout::Error => e
|
496
|
+
log "error sending email %d: %p(%s):\n\t%s" %
|
497
|
+
[email.id, e.message, e.class, e.backtrace.join("\n\t")]
|
498
|
+
session.reset
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
rescue Net::SMTPAuthenticationError => e
|
503
|
+
@failed_auth_count += 1
|
504
|
+
if @failed_auth_count >= MAX_AUTH_FAILURES then
|
505
|
+
log "authentication error, giving up: #{e.message}"
|
506
|
+
raise e
|
507
|
+
else
|
508
|
+
log "authentication error, retrying: #{e.message}"
|
509
|
+
end
|
510
|
+
sleep delay
|
511
|
+
rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError
|
512
|
+
# ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
|
513
|
+
end
|
514
|
+
|
412
515
|
##
|
413
516
|
# Prepares ar_sendmail for exiting
|
414
517
|
|
@@ -430,7 +533,31 @@ class ActionMailer::ARSendmail
|
|
430
533
|
log "found #{mail.length} emails to send"
|
431
534
|
mail
|
432
535
|
end
|
433
|
-
|
536
|
+
|
537
|
+
def find_newsletter
|
538
|
+
|
539
|
+
options = { :conditions => ['cancelled != 1 AND completed != 1'] }
|
540
|
+
newsletter = ActionMailer::Base.newsletter_class.find :first, options
|
541
|
+
|
542
|
+
log "found newsletter with title: #{newsletter.title}" unless newsletter.nil?
|
543
|
+
log "no newsletters found" unless !newsletter.nil?
|
544
|
+
newsletter
|
545
|
+
|
546
|
+
end
|
547
|
+
|
548
|
+
def find_users(limit, offset)
|
549
|
+
|
550
|
+
options = { :conditions => ['newsletter = 1 AND email IS NOT NULL'], :limit => limit, :offset => offset, :order => "id" }
|
551
|
+
users = ActionMailer::Base.user_class.find :all, options
|
552
|
+
|
553
|
+
if !users.nil? && users.length < limit
|
554
|
+
|
555
|
+
end
|
556
|
+
log "found #{users.length} users to send a newsletter"
|
557
|
+
users
|
558
|
+
|
559
|
+
end
|
560
|
+
|
434
561
|
##
|
435
562
|
# Installs signal handlers to gracefully exit.
|
436
563
|
|
@@ -457,8 +584,35 @@ class ActionMailer::ARSendmail
|
|
457
584
|
loop do
|
458
585
|
begin
|
459
586
|
cleanup
|
587
|
+
|
460
588
|
emails = find_emails
|
589
|
+
already_send = emails.empty? ? 0 : emails.length
|
461
590
|
deliver(emails) unless emails.empty?
|
591
|
+
|
592
|
+
newsletter = find_newsletter
|
593
|
+
|
594
|
+
unless newsletter.nil? || batch_size.nil?
|
595
|
+
offset = newsletter.mails_send.nil? ? 0 : newsletter.mails_send
|
596
|
+
limit = batch_size - already_send
|
597
|
+
|
598
|
+
users = find_users(limit, offset)
|
599
|
+
|
600
|
+
update = {}
|
601
|
+
update[:mails_send] = offset + users.length unless users.empty?
|
602
|
+
|
603
|
+
if (!users.empty? && users.length < limit) || users.empty?
|
604
|
+
update[:completed] = 1
|
605
|
+
end
|
606
|
+
|
607
|
+
# Deliver newsletter
|
608
|
+
newsletter.update_attributes(update) unless @dry_run
|
609
|
+
deliver_newsletter(newsletter, users) unless users.empty?
|
610
|
+
|
611
|
+
end
|
612
|
+
|
613
|
+
# Check for bounces
|
614
|
+
check_bounces unless @dry_run
|
615
|
+
|
462
616
|
rescue ActiveRecord::Transactions::TransactionError
|
463
617
|
end
|
464
618
|
break if @once
|
@@ -478,4 +632,92 @@ class ActionMailer::ARSendmail
|
|
478
632
|
ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
|
479
633
|
end
|
480
634
|
|
635
|
+
def check_bounces
|
636
|
+
|
637
|
+
if @bouncecheck
|
638
|
+
begin
|
639
|
+
imap = Net::IMAP.new(@imap[:host], @imap[:port], true)
|
640
|
+
imap.login(@imap[:user], @imap[:password])
|
641
|
+
imap.select('INBOX')
|
642
|
+
|
643
|
+
imap.uid_search(["NOT", "DELETED"]).each do |message_id|
|
644
|
+
msg = imap.uid_fetch(message_id,'RFC822')[0].attr['RFC822']
|
645
|
+
email = TMail::Mail.parse(msg)
|
646
|
+
receive(email)
|
647
|
+
#Mark message as deleted and it will be removed from storage when user session closed
|
648
|
+
imap.uid_store(message_id, "+FLAGS", [:Deleted])
|
649
|
+
end
|
650
|
+
# tell server to permanently remove all messages flagged as :Deleted
|
651
|
+
imap.expunge
|
652
|
+
imap.logout
|
653
|
+
imap.disconnect
|
654
|
+
rescue Net::IMAP::NoResponseError => e
|
655
|
+
log e
|
656
|
+
rescue Net::IMAP::ByeResponseError => e
|
657
|
+
log e
|
658
|
+
rescue => e
|
659
|
+
log e
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
end
|
664
|
+
|
665
|
+
def receive(email)
|
666
|
+
bounce = BouncedDelivery.from_email(email)
|
667
|
+
if(bounce.status_info > 3)
|
668
|
+
log "User bounced with email: #{bounce.sender}"
|
669
|
+
user = ActionMailer::Base.user_class.find_by_email(bounce.sender)
|
670
|
+
|
671
|
+
if !user.nil?
|
672
|
+
user.update_attribute(:bounced_at, Time.now)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
481
677
|
end
|
678
|
+
|
679
|
+
##
|
680
|
+
# Checking for bounced delivery
|
681
|
+
#
|
682
|
+
|
683
|
+
class BouncedDelivery
|
684
|
+
|
685
|
+
attr_accessor :status_info, :sender, :subject
|
686
|
+
|
687
|
+
def self.from_email(email)
|
688
|
+
returning(bounce = self.new) do
|
689
|
+
|
690
|
+
if(email.subject.match(/Mail delivery failed/i))
|
691
|
+
bounce.status_info = 6
|
692
|
+
elsif(email.subject.match(/Delivery Status Notification/i))
|
693
|
+
bounce.status_info = 8
|
694
|
+
elsif(email.header.to_s.match(/X-Failed-Recipient/i))
|
695
|
+
bounce.status_info = 10
|
696
|
+
else
|
697
|
+
bounce.status_info = 2
|
698
|
+
end
|
699
|
+
|
700
|
+
if bounce.status_info > 3
|
701
|
+
bounce.subject = email.subject
|
702
|
+
bounce.sender = bounce.check_recipient(email.header)
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
def check_recipient(header)
|
708
|
+
header['x-failed-recipients'].body.to_s unless header['x-failed-recipients'].nil?
|
709
|
+
end
|
710
|
+
|
711
|
+
def status
|
712
|
+
case status_info
|
713
|
+
when 10
|
714
|
+
'Failed - X-Failed-Recipient'
|
715
|
+
when 8
|
716
|
+
'Failed - Delivery Status Notification'
|
717
|
+
when 6
|
718
|
+
'Failed - Mail delivery failed'
|
719
|
+
when 2
|
720
|
+
'Success'
|
721
|
+
end
|
722
|
+
end
|
723
|
+
end
|
data/lib/adzap-ar_mailer.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: VvanGemert-ar_mailer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 2.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 2.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Eric Hodel
|
@@ -18,39 +18,8 @@ cert_chain: []
|
|
18
18
|
|
19
19
|
date: 2010-03-17 00:00:00 +01:00
|
20
20
|
default_executable: ar_sendmail
|
21
|
-
dependencies:
|
22
|
-
|
23
|
-
name: minitest
|
24
|
-
prerelease: false
|
25
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
-
none: false
|
27
|
-
requirements:
|
28
|
-
- - ">="
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
hash: 3
|
31
|
-
segments:
|
32
|
-
- 1
|
33
|
-
- 5
|
34
|
-
- 0
|
35
|
-
version: 1.5.0
|
36
|
-
type: :development
|
37
|
-
version_requirements: *id001
|
38
|
-
- !ruby/object:Gem::Dependency
|
39
|
-
name: mocha
|
40
|
-
prerelease: false
|
41
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
-
none: false
|
43
|
-
requirements:
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
hash: 43
|
47
|
-
segments:
|
48
|
-
- 0
|
49
|
-
- 9
|
50
|
-
- 8
|
51
|
-
version: 0.9.8
|
52
|
-
type: :development
|
53
|
-
version_requirements: *id002
|
21
|
+
dependencies: []
|
22
|
+
|
54
23
|
description: Even delivering email to the local machine may take too long when you have to send hundreds of messages. ar_mailer allows you to store messages into the database for later delivery by a separate process, ar_sendmail.
|
55
24
|
email: vincent@floorplanner.com
|
56
25
|
executables:
|