smailer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +25 -0
- data/Rakefile +2 -0
- data/lib/smailer.rb +5 -0
- data/lib/smailer/models.rb +10 -0
- data/lib/smailer/models/finished_mail.rb +41 -0
- data/lib/smailer/models/mail_campaign.rb +54 -0
- data/lib/smailer/models/mail_key.rb +42 -0
- data/lib/smailer/models/mailing_list.rb +13 -0
- data/lib/smailer/models/property.rb +54 -0
- data/lib/smailer/models/queued_mail.rb +41 -0
- data/lib/smailer/tasks.rb +5 -0
- data/lib/smailer/tasks/send.rb +59 -0
- data/lib/smailer/version.rb +3 -0
- data/smailer.gemspec +22 -0
- metadata +93 -0
data/Gemfile
ADDED
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
data/lib/smailer.rb
ADDED
@@ -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,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
|
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
|
+
|