VvanGemert-ar_mailer 2.1.8 → 2.2.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/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:
|