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.
@@ -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 = ["test/test_armailer.rb", "test/test_arsendmail.rb"]
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
- res = session.send_message email.mail, email.from, email.to
379
- email.destroy
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
@@ -1 +1,2 @@
1
1
  require 'action_mailer/ar_mailer'
2
+ require 'action_mailer/ar_newsletter'
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: 27
4
+ hash: 7
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
- - 1
9
- - 8
10
- version: 2.1.8
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
- - !ruby/object:Gem::Dependency
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: