smailer 0.1.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/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ *.tmproj
5
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :gemcutter
2
+
3
+ # Specify your gem's dependencies in smailer.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Simple newsletter mailer for Rails
2
+
3
+ ## Intro
4
+
5
+ This project is a simple mailer for newsletters, which implements simple queue processing, basic campaign management and has some unsubscribe support.
6
+
7
+ It is intended to be used within a Rails project.
8
+
9
+ ## Install
10
+
11
+ For Rails 3 projects, update your Gemfile and add:
12
+ gem 'smailer'
13
+ Then run `bundle install` and you should be ready to go.
14
+
15
+ ## Usage and documentation
16
+
17
+ TODO
18
+
19
+ ## Contribution
20
+
21
+ Patches are always welcome. In case you find any issues with this code, please use the project's [Issues](http://github.com/mitio/smailer/issues) page on Github to report them. Feel free to contribute! :)
22
+
23
+ ## License
24
+
25
+ Released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/lib/smailer.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Smailer
2
+ autoload :VERSION, 'smailer/version'
3
+ autoload :Models, 'smailer/models'
4
+ autoload :Tasks, 'smailer/tasks'
5
+ end
@@ -0,0 +1,10 @@
1
+ module Smailer
2
+ module Models
3
+ autoload :MailingList, 'smailer/models/mailing_list'
4
+ autoload :MailCampaign, 'smailer/models/mail_campaign'
5
+ autoload :MailKey, 'smailer/models/mail_key'
6
+ autoload :QueuedMail, 'smailer/models/queued_mail'
7
+ autoload :FinishedMail, 'smailer/models/finished_mail'
8
+ autoload :Property, 'smailer/models/property'
9
+ end
10
+ end
@@ -0,0 +1,41 @@
1
+ module Smailer
2
+ module Models
3
+ class FinishedMail < ActiveRecord::Base
4
+ class Statuses
5
+ FAILED = 0
6
+ SENT = 1
7
+ end
8
+
9
+ belongs_to :mail_campaign
10
+
11
+ validates_presence_of :mail_campaign_id, :from, :to, :retries, :status
12
+ validates_numericality_of :mail_campaign_id, :retries, :status, :only_integer => true, :allow_nil => true
13
+ validates_length_of :from, :to, :subject, :last_error, :maximum => 255
14
+
15
+ delegate :mailing_list, :to => :mail_campaign, :allow_nil => true
16
+
17
+ def status_text
18
+ Statuses.constants.each do |constant_name|
19
+ return constant_name if Statuses.const_get(constant_name) == status
20
+ end
21
+ nil
22
+ end
23
+
24
+ def self.add(queued_mail, status = Statuses::SENT)
25
+ finished = self.new
26
+
27
+ [:mail_campaign_id, :from, :to, :subject, :body_html, :body_text, :retries, :last_retry_at, :last_error].each do |field|
28
+ finished.send("#{field}=", queued_mail.send(field))
29
+ end
30
+
31
+ finished.status = status
32
+ finished.sent_at = Time.now if status == Statuses::SENT
33
+
34
+ finished.save!
35
+ queued_mail.destroy
36
+
37
+ finished
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ module Smailer
2
+ module Models
3
+ class MailCampaign < ActiveRecord::Base
4
+ class UnsubscribeMethods
5
+ URL = 1
6
+ REPLY = 2
7
+ BOUNCED = 4
8
+ end
9
+
10
+ belongs_to :mailing_list
11
+ has_many :queued_mails, :dependent => :destroy
12
+ has_many :finished_mails
13
+
14
+ validates_presence_of :mailing_list_id, :from
15
+ validates_numericality_of :mailing_list_id, :unsubscribe_methods, :only_integer => true, :allow_nil => true
16
+ validates_length_of :from, :subject, :maximum => 255, :allow_nil => true
17
+
18
+ attr_accessible :mailing_list_id, :from, :subject, :body_html, :body_text
19
+
20
+ def add_unsubscribe_method(method)
21
+ self.unsubscribe_methods = (self.unsubscribe_methods || 0) | method
22
+ end
23
+
24
+ def remove_unsubscribe_method(method)
25
+ if has_unsubscribe_method?(method)
26
+ self.unsubscribe_methods = (self.unsubscribe_methods || 0) ^ method
27
+ end
28
+ end
29
+
30
+ def has_unsubscribe_method?(method)
31
+ (unsubscribe_methods || 0) & method === method
32
+ end
33
+
34
+ def active_unsubscribe_methods
35
+ self.class.unsubscribe_methods.reject do |method, method_name|
36
+ not has_unsubscribe_method?(method)
37
+ end
38
+ end
39
+
40
+ def name
41
+ "Campaign ##{id} (#{mailing_list.name})"
42
+ end
43
+
44
+ def self.unsubscribe_methods
45
+ methods = {}
46
+ UnsubscribeMethods.constants.map do |method_name|
47
+ methods[UnsubscribeMethods.const_get(method_name)] = method_name
48
+ end
49
+
50
+ methods
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ require 'digest/md5'
2
+
3
+ module Smailer
4
+ module Models
5
+ class MailKey < ActiveRecord::Base
6
+ validates_presence_of :email, :key
7
+ validates_uniqueness_of :email
8
+ validates_uniqueness_of :key
9
+ validates_length_of :email, :key, :maximum => 255
10
+
11
+ attr_accessible :email, :key
12
+
13
+ before_validation :set_key
14
+ before_save :set_key
15
+
16
+ def self.generate(email)
17
+ Digest::MD5.hexdigest("The #{email} and our great secret, which lies in Mt. Asgard!")
18
+ end
19
+
20
+ def self.get(email)
21
+ email = extract_email(email)
22
+ key = generate(email)
23
+ stored = where(:key => key).first
24
+
25
+ create :email => email, :key => key unless stored
26
+
27
+ key
28
+ end
29
+
30
+ def self.extract_email(email)
31
+ email = $1.strip if email =~ /<(.+@.+)>\s*$/
32
+ email
33
+ end
34
+
35
+ protected
36
+
37
+ def set_key
38
+ self.key = self.class.generate(email)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Smailer
2
+ module Models
3
+ class MailingList < ActiveRecord::Base
4
+ has_many :mail_campaigns
5
+
6
+ validates_presence_of :name
7
+ validates_uniqueness_of :name
8
+ validates_length_of :name, :maximum => 255
9
+
10
+ attr_accessible :name
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ module Smailer
2
+ module Models
3
+ class Property < ActiveRecord::Base
4
+ set_table_name 'smailer_properties'
5
+
6
+ validates_presence_of :name
7
+ validates_uniqueness_of :name
8
+
9
+ attr_accessible :name, :value, :notes
10
+
11
+ @@cache_created_at = Time.now
12
+
13
+ def self.clear_cache_if_needed!
14
+ if Time.now - @@cache_created_at > 1.minute
15
+ remove_class_variable :@@properties rescue nil
16
+ @@cache_created_at = Time.now
17
+ end
18
+ end
19
+
20
+ def self.setup_cache!
21
+ self.clear_cache_if_needed!
22
+ @@properties ||= {} unless defined?(@@properties)
23
+ end
24
+
25
+ def self.get(name)
26
+ self.setup_cache!
27
+ name = name.to_s
28
+
29
+ unless @@properties.has_key?(name)
30
+ property = find_by_name name
31
+ @@properties[name] = property ? property.value : nil
32
+ end
33
+
34
+ @@properties[name]
35
+ end
36
+
37
+ def self.get_all(name)
38
+ self.setup_cache!
39
+ name, cache_key = name.to_s, "#{name}[]"
40
+
41
+ unless @@properties.has_key?(cache_key)
42
+ properties = all(:conditions => ['name LIKE ?', "#{name}%"]).map { |p| [p.name, p.value] }
43
+ @@properties[cache_key] = Hash[ properties ]
44
+ end
45
+
46
+ @@properties[cache_key]
47
+ end
48
+
49
+ def self.get_boolean(name)
50
+ self.get(name).to_s.strip =~ /^(true|t|yes|y|on|1)$/i ? true : false
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ module Smailer
2
+ module Models
3
+ class QueuedMail < ActiveRecord::Base
4
+ belongs_to :mail_campaign
5
+
6
+ validates_presence_of :mail_campaign_id, :to
7
+ validates_uniqueness_of :to, :scope => :mail_campaign_id
8
+ validates_numericality_of :mail_campaign_id, :retries, :only_integer => true, :allow_nil => true
9
+ validates_length_of :to, :last_error, :maximum => 255
10
+
11
+ attr_accessible :mail_campaign_id, :to
12
+
13
+ delegate :from, :subject, :mailing_list, :to => :mail_campaign, :allow_nil => true
14
+
15
+ def body_html
16
+ interpolate mail_campaign.body_html
17
+ end
18
+
19
+ def body_text
20
+ interpolate mail_campaign.body_text
21
+ end
22
+
23
+ protected
24
+
25
+ def interpolate(text)
26
+ {
27
+ :email => to,
28
+ :escaped_email => lambda { ERB::Util.h(to) },
29
+ :email_key => lambda { MailKey.get(to) },
30
+ :mailing_list_id => lambda { mailing_list.id },
31
+ }.each do |variable, interpolation|
32
+ text.gsub! "%{#{variable}}" do
33
+ interpolation.respond_to?(:call) ? interpolation.call : interpolation.to_s
34
+ end
35
+ end
36
+
37
+ text
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ module Smailer
2
+ module Tasks
3
+ autoload :Send, 'smailer/tasks/send'
4
+ end
5
+ end
@@ -0,0 +1,59 @@
1
+ require 'mail'
2
+
3
+ module Smailer
4
+ module Tasks
5
+ class Send
6
+ def self.execute
7
+ Mail.defaults do
8
+ delivery_method Rails.configuration.action_mailer.delivery_method
9
+ end
10
+
11
+ batch_size = Smailer::Models::Property.get('queue.batch_size').to_i
12
+ max_retries = Smailer::Models::Property.get('queue.max_retries').to_i
13
+ max_lifetime = Smailer::Models::Property.get('queue.max_lifetime').to_i
14
+
15
+ results = []
16
+ Smailer::Models::QueuedMail.order(:retries.asc, :id.asc).limit(batch_size).each do |queue_item|
17
+ # try to send the email
18
+ mail = Mail.new do
19
+ from queue_item.from
20
+ to queue_item.to
21
+ subject queue_item.subject
22
+
23
+ text_part { body queue_item.body_text }
24
+ html_part { body queue_item.body_html; content_type 'text/html; charset=UTF-8' }
25
+ end
26
+ mail.raise_delivery_errors = true
27
+
28
+ queue_item.last_retry_at = Time.now
29
+ queue_item.retries += 1
30
+
31
+ begin
32
+ # commense delivery
33
+ mail.deliver
34
+ rescue Exception => e
35
+ # failed, we have.
36
+ queue_item.last_error = "#{e.class.name}: #{e.message}"
37
+ queue_item.save
38
+
39
+ # check if the message hasn't expired;
40
+ retries_exceeded = max_retries > 0 && queue_item.retries >= max_retries
41
+ too_old = max_lifetime > 0 && (Time.now - queue_item.created_at) >= max_lifetime
42
+
43
+ if retries_exceeded || too_old
44
+ # the message has expired; move to finished_mails
45
+ Smailer::Models::FinishedMail.add(queue_item, Smailer::Models::FinishedMail::Statuses::FAILED)
46
+ end
47
+ results.push [queue_item, :failed]
48
+ else
49
+ # great job, message sent
50
+ Smailer::Models::FinishedMail.add(queue_item, Smailer::Models::FinishedMail::Statuses::SENT)
51
+ results.push [queue_item, :sent]
52
+ end
53
+ end
54
+
55
+ results
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Smailer
2
+ VERSION = "0.1.0"
3
+ end
data/smailer.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/smailer/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "smailer"
6
+ s.version = Smailer::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Dimitar Dimitrov']
9
+ s.email = ['wireman@gmail.com']
10
+ s.homepage = "http://github.com/mitio/smailer"
11
+ s.summary = "A simple newsletter mailer for Rails."
12
+ s.description = "A simple mailer for newsletters with basic campaign management, queue and unsubscribe support. To be used within Rails."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.rubyforge_project = "smailer"
16
+
17
+ s.add_development_dependency "bundler", ">= 1.0.0"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
21
+ s.require_path = 'lib'
22
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smailer
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Dimitar Dimitrov
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-09-02 00:00:00 +03:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bundler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 0
30
+ - 0
31
+ version: 1.0.0
32
+ type: :development
33
+ version_requirements: *id001
34
+ description: A simple mailer for newsletters with basic campaign management, queue and unsubscribe support. To be used within Rails.
35
+ email:
36
+ - wireman@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - README.md
47
+ - Rakefile
48
+ - lib/smailer.rb
49
+ - lib/smailer/models.rb
50
+ - lib/smailer/models/finished_mail.rb
51
+ - lib/smailer/models/mail_campaign.rb
52
+ - lib/smailer/models/mail_key.rb
53
+ - lib/smailer/models/mailing_list.rb
54
+ - lib/smailer/models/property.rb
55
+ - lib/smailer/models/queued_mail.rb
56
+ - lib/smailer/tasks.rb
57
+ - lib/smailer/tasks/send.rb
58
+ - lib/smailer/version.rb
59
+ - smailer.gemspec
60
+ has_rdoc: true
61
+ homepage: http://github.com/mitio/smailer
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 1
82
+ - 3
83
+ - 6
84
+ version: 1.3.6
85
+ requirements: []
86
+
87
+ rubyforge_project: smailer
88
+ rubygems_version: 1.3.6
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: A simple newsletter mailer for Rails.
92
+ test_files: []
93
+