replyr 0.0.7 → 0.0.8
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +41 -5
- data/lib/generators/replyr/templates/mailman_server +3 -3
- data/lib/generators/replyr/templates/replyr.rb.erb +12 -3
- data/lib/replyr.rb +31 -4
- data/lib/replyr/address_builder.rb +69 -0
- data/lib/replyr/bounce_address.rb +52 -0
- data/lib/replyr/config.rb +18 -5
- data/lib/replyr/{reply_email.rb → email.rb} +20 -4
- data/lib/replyr/engine.rb +1 -0
- data/lib/replyr/handle_bounce.rb +35 -0
- data/lib/replyr/reply_address.rb +7 -68
- data/lib/replyr/version.rb +1 -1
- data/test/dummy/app/models/user.rb +5 -0
- data/test/dummy/config/initializers/replyr.rb +2 -1
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +16508 -0
- data/test/replyr/bounce_address_test.rb +91 -0
- data/test/replyr/email_test.rb +93 -0
- data/test/replyr/emails/bounce_422.eml +98 -0
- data/test/replyr/emails/bounce_530.eml +96 -0
- data/test/replyr/handle_bounce_test.rb +33 -0
- data/test/replyr/handle_reply_test.rb +1 -1
- data/test/test_helper.rb +1 -1
- metadata +11 -4
- data/test/replyr/reply_email_test.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 09c0af70b27c7e54b91f2df46e1f397d57449109
|
4
|
+
data.tar.gz: b00e0b4df98e524e279235a580159b85b6714000
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2667bea0a35a65e7fde2635b636de4a99e33972e55425cd9be7f39835187add195525717296296cc54d630e4d55c394e511a9598a4a80dc3314937faba3f70a
|
7
|
+
data.tar.gz: ef21000d33e3b10ca64293290ca3699554c46d83f14653d4495e756bd8a78e689300bbc73ebae3b732f6216ffe0e0b26afa34da45d4cb767a1b6e2fda047868a
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -25,7 +25,7 @@ https://github.com/titanous/mailman
|
|
25
25
|
- Ruby >= 1.9.3
|
26
26
|
- Rails 3 >= 3.1 or Rails 4.x
|
27
27
|
|
28
|
-
##
|
28
|
+
## Installation
|
29
29
|
|
30
30
|
#### Add the gem to your `Gemfile`
|
31
31
|
|
@@ -45,7 +45,7 @@ $ rails g replyr:install
|
|
45
45
|
Open up `config/initializers/replyr.rb` and set the host name of your reply email address.
|
46
46
|
|
47
47
|
```ruby
|
48
|
-
Ryplr.config.
|
48
|
+
Ryplr.config.reply_host = "yourdomain.com"
|
49
49
|
```
|
50
50
|
|
51
51
|
#### Setup Mailman Gem
|
@@ -54,6 +54,8 @@ The Install Generator will already have created a `script/mailman_server` file w
|
|
54
54
|
|
55
55
|
## Usage
|
56
56
|
|
57
|
+
### Reply Handling
|
58
|
+
|
57
59
|
#### Make a model accept replies
|
58
60
|
|
59
61
|
Update your ActiveRecord models you want to reply to by adding a `handle_reply` like this:
|
@@ -65,6 +67,7 @@ class Comment < ActiveRecord::Base
|
|
65
67
|
handle_reply do |comment, user, text, files|
|
66
68
|
Comment.create(body: text)
|
67
69
|
end
|
70
|
+
|
68
71
|
end
|
69
72
|
```
|
70
73
|
|
@@ -78,15 +81,48 @@ To add the unique reply address to your outgoing emails and make them 'replyable
|
|
78
81
|
|
79
82
|
```ruby
|
80
83
|
class CommentMailer < ActionMailer::Base
|
84
|
+
|
81
85
|
def new_comment(user, comment)
|
82
|
-
mail
|
86
|
+
mail to: user.email, reply_to: comment.reply_address_for_user(user)
|
83
87
|
end
|
88
|
+
|
84
89
|
end
|
85
90
|
```
|
86
91
|
|
87
|
-
|
92
|
+
### Bounce Handling
|
93
|
+
|
94
|
+
#### Make a model accept bounce emails
|
95
|
+
|
96
|
+
Add a `handle_bounce` call to the ActiveRecord model you want to handle your bounce emails on.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class User < ActiveRecord::Base
|
100
|
+
|
101
|
+
handle_bounce do |user, email|
|
102
|
+
# Put your custom bounce handling code here
|
103
|
+
# e.g. mark email as invalid
|
104
|
+
user.update_attribute(:email_valid, false)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Update your mailers and set the `return_path`
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class CommentMailer < ActionMailer::Base
|
114
|
+
|
115
|
+
def new_comment(user, comment)
|
116
|
+
mail to: user.email, return_path: user.bounce_address
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
|
123
|
+
## Start up the worker
|
88
124
|
|
89
|
-
Start/Stop
|
125
|
+
Start/Stop the Mailman background worker with the following commands:
|
90
126
|
|
91
127
|
```bash
|
92
128
|
$ RAILS_ENV=production script/mailman_daemon start
|
@@ -34,11 +34,11 @@ Mailman.config.maildir = "~/Maildir"
|
|
34
34
|
Mailman::Application.run do
|
35
35
|
|
36
36
|
# DO NOT CHANGE. Replyr Route for processing reply emails
|
37
|
-
to Replyr.
|
38
|
-
Replyr
|
37
|
+
to Replyr.route do
|
38
|
+
Replyr.process(message)
|
39
39
|
end
|
40
40
|
|
41
|
-
# add your custom routes (independent of Replyr) here
|
41
|
+
# add your custom mailman routes (independent of Replyr) here
|
42
42
|
# to 'support%@example.org' do
|
43
43
|
# # ...
|
44
44
|
# end
|
@@ -1,8 +1,17 @@
|
|
1
1
|
# Configuration for Replyr
|
2
2
|
#
|
3
3
|
|
4
|
-
# Set the host name for your reply email addresses
|
5
|
-
Replyr.config.host = "example.com"
|
6
|
-
|
7
4
|
# Set a secure secret token to be used for salting the email-address
|
8
5
|
Replyr.config.secret = "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
|
6
|
+
|
7
|
+
# Set the host name for your reply email addresses
|
8
|
+
Replyr.config.reply_host = "example.com"
|
9
|
+
|
10
|
+
# Set the host name for your bounce email addresses
|
11
|
+
Replyr.config.bounce_host = "example.com"
|
12
|
+
|
13
|
+
# Set custom prefix for your reply email addresses (default: "reply")
|
14
|
+
# Replyr.config.reply_prefix = "reply"
|
15
|
+
|
16
|
+
# Set custom prefix for your bounce email addresses (default: "bounce")
|
17
|
+
# Replyr.config.bounce_prefix = "bounce"
|
data/lib/replyr.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'mailman'
|
2
2
|
require 'email_reply_parser/email_reply_parser'
|
3
3
|
require "replyr/config"
|
4
|
-
|
4
|
+
|
5
|
+
require "replyr/address_builder"
|
5
6
|
require "replyr/reply_address"
|
7
|
+
require "replyr/bounce_address"
|
8
|
+
|
9
|
+
require "replyr/email"
|
10
|
+
|
6
11
|
require "replyr/handle_reply"
|
12
|
+
require "replyr/handle_bounce"
|
7
13
|
require 'replyr/engine'
|
8
14
|
|
9
15
|
# Monkey Patch broken listen dependency in mailman v0.7.0
|
@@ -21,10 +27,31 @@ module Replyr
|
|
21
27
|
@logger = (defined?(Rails) && Rails.logger) ? Rails.logger : Logger.new(STDOUT)
|
22
28
|
end
|
23
29
|
|
24
|
-
|
25
|
-
|
30
|
+
# Regexp for reply addresses:
|
31
|
+
# reply-comment-12-56-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@reply.example.com
|
32
|
+
#
|
33
|
+
def reply_pattern
|
34
|
+
/#{config.reply_prefix}-(?<model_name>[a-z,#]+)-(?<model_id>\d+)-(?<user_id>\d+)-(?<token>\S+)@#{config.reply_host}/
|
35
|
+
end
|
36
|
+
|
37
|
+
# Regexp for bounce addresses:
|
38
|
+
# bounce-newsletter-12-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@bounce.example.com
|
39
|
+
#
|
40
|
+
def bounce_pattern
|
41
|
+
/#{config.bounce_prefix}-(?<model_name>[a-z,#]+)-(?<model_id>\d+)-(?<token>\S+)@#{config.bounce_host}/
|
42
|
+
end
|
43
|
+
|
44
|
+
# Regexp for bounce and reply addresses.
|
45
|
+
# Use this as the Replyr route in your mailman-server.
|
46
|
+
#
|
47
|
+
def route
|
48
|
+
/#{reply_pattern}|#{bounce_pattern}/
|
49
|
+
end
|
50
|
+
alias_method :address_pattern, :route
|
51
|
+
|
52
|
+
def process(message)
|
53
|
+
Replyr::Email.process(message)
|
26
54
|
end
|
27
|
-
alias_method :mailman_route, :address_pattern
|
28
55
|
|
29
56
|
end
|
30
57
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Replyr
|
4
|
+
module AddressBuilder
|
5
|
+
# Model name may be namespaced (e.g. MyApp::Comment)
|
6
|
+
# For the reply email address the model name will be converted to "my_app/comment".
|
7
|
+
# Then the slash "/" will be replaced with a plus sign "+", because
|
8
|
+
# slashes should not be used email addresses.
|
9
|
+
#
|
10
|
+
def normalized_model_name(model)
|
11
|
+
model.class.name.tableize.singularize.gsub("/", "+")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Converts a normalized model_name back to a real model name
|
15
|
+
# and returns the class
|
16
|
+
#
|
17
|
+
def self.class_from_normalized_model_name(model_name)
|
18
|
+
model_name.gsub("+", "/").classify.constantize
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the ID of an AR object.
|
22
|
+
# Uses primary key to find out the correct field
|
23
|
+
#
|
24
|
+
def id_from_model(object)
|
25
|
+
object.send(object.class.primary_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Split the reply/bounce email address. It has the following format:
|
29
|
+
# Reply: reply-comment-12-56-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@reply.example.com
|
30
|
+
# Bounce: bounce-newsletter-12-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@bounce.example.com
|
31
|
+
#
|
32
|
+
def self.get_parsed_address(address)
|
33
|
+
if match_data = Replyr.route.match(address)
|
34
|
+
match_data
|
35
|
+
else
|
36
|
+
raise ArgumentError, "Malformed reply/bounce email address."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Creates a token from the passed model (and user if passed)
|
41
|
+
# Uses the configured secret as a salt
|
42
|
+
#
|
43
|
+
def create_token(model, user = nil)
|
44
|
+
user_id = user.present? ? id_from_model(user) : nil
|
45
|
+
model_id = id_from_model(model)
|
46
|
+
model_name = normalized_model_name(model)
|
47
|
+
|
48
|
+
OpenSSL::HMAC.hexdigest(
|
49
|
+
OpenSSL::Digest.new('sha1'),
|
50
|
+
Replyr.config.secret,
|
51
|
+
[user_id, model_name, model_id].compact.join("-")
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if a given token is valid
|
56
|
+
#
|
57
|
+
def token_valid?(token)
|
58
|
+
token == self.token
|
59
|
+
end
|
60
|
+
|
61
|
+
# Ensure a given token is valid
|
62
|
+
#
|
63
|
+
def ensure_valid_token!(token)
|
64
|
+
raise(RuntimeError, "Token invalid.") unless token_valid?(token)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Replyr
|
4
|
+
class BounceAddress
|
5
|
+
include AddressBuilder
|
6
|
+
|
7
|
+
attr_accessor :model
|
8
|
+
|
9
|
+
# Create a new reply address from a given user and model
|
10
|
+
#
|
11
|
+
def initialize(model)
|
12
|
+
@model = model
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create a reply address from a given address string
|
16
|
+
# Checks for validity of address and raises an ArgumentError
|
17
|
+
# if it's invalid.
|
18
|
+
#
|
19
|
+
def self.new_from_address(address)
|
20
|
+
parts = AddressBuilder.get_parsed_address(address)
|
21
|
+
|
22
|
+
model_class = AddressBuilder.class_from_normalized_model_name(parts[:model_name])
|
23
|
+
model = model_class.find(parts[:model_id])
|
24
|
+
|
25
|
+
address = new(model)
|
26
|
+
address.ensure_valid_token!(parts[:token])
|
27
|
+
address
|
28
|
+
rescue
|
29
|
+
Replyr.logger.warn "Bounce email address invalid."
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returs the token from this address
|
34
|
+
#
|
35
|
+
def token
|
36
|
+
create_token(@model)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the address string
|
40
|
+
# (e.g bounce-newsletter-12-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@bounce.example.com)
|
41
|
+
#
|
42
|
+
def address
|
43
|
+
model_id = id_from_model(@model)
|
44
|
+
model_name = normalized_model_name(@model)
|
45
|
+
|
46
|
+
local_part = [Replyr.config.bounce_prefix, model_name, model_id, token].join("-")
|
47
|
+
"#{local_part}@#{Replyr.config.bounce_host}"
|
48
|
+
end
|
49
|
+
alias_method :to_s, :address
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/replyr/config.rb
CHANGED
@@ -1,17 +1,30 @@
|
|
1
1
|
module Replyr
|
2
2
|
class Config
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :reply_prefix,
|
4
|
+
:reply_host,
|
5
|
+
:bounce_prefix,
|
6
|
+
:bounce_host,
|
7
|
+
:secret,
|
8
|
+
:user_class
|
4
9
|
|
5
|
-
def
|
6
|
-
@
|
10
|
+
def reply_prefix
|
11
|
+
@reply_prefix || "reply"
|
12
|
+
end
|
13
|
+
|
14
|
+
def bounce_prefix
|
15
|
+
@bounce_prefix || "bounce"
|
7
16
|
end
|
8
17
|
|
9
18
|
def user_class
|
10
19
|
@user_class || User
|
11
20
|
end
|
12
21
|
|
13
|
-
def
|
14
|
-
@
|
22
|
+
def reply_host
|
23
|
+
@reply_host || (raise RuntimeError, "Replyr.config.reply_host is nil. Please set a host in an initializer.")
|
24
|
+
end
|
25
|
+
|
26
|
+
def bounce_host
|
27
|
+
@bounce_host || (raise RuntimeError, "Replyr.config.bounce_host is nil. Please set a host in an initializer.")
|
15
28
|
end
|
16
29
|
|
17
30
|
def secret
|
@@ -1,8 +1,9 @@
|
|
1
1
|
module Replyr
|
2
|
-
class
|
3
|
-
attr_accessor :to, :from, :subject, :body, :files
|
2
|
+
class Email
|
3
|
+
attr_accessor :to, :from, :subject, :body, :files, :mail
|
4
4
|
|
5
5
|
def initialize(mail)
|
6
|
+
self.mail = mail
|
6
7
|
self.to = mail.to.first
|
7
8
|
self.from = mail.from.first
|
8
9
|
self.subject = mail.subject
|
@@ -28,15 +29,30 @@ module Replyr
|
|
28
29
|
# Checks if this incoming mail is a reply email
|
29
30
|
#
|
30
31
|
def is_reply_email?
|
31
|
-
to.starts_with?(Replyr.config.
|
32
|
+
to.starts_with?(Replyr.config.reply_prefix)
|
32
33
|
end
|
33
34
|
|
34
35
|
def stripped_body
|
35
36
|
EmailReplyParser.parse_reply(body, from).to_s.force_encoding("UTF-8")
|
36
37
|
end
|
37
38
|
|
39
|
+
def is_bounce_email?
|
40
|
+
mail.bounced? || false
|
41
|
+
end
|
42
|
+
|
43
|
+
def failed_permanently?
|
44
|
+
!mail.retryable? || false
|
45
|
+
end
|
46
|
+
|
38
47
|
def process
|
39
|
-
if
|
48
|
+
if is_bounce_email?
|
49
|
+
if bounce_address = BounceAddress.new_from_address(to)
|
50
|
+
bounce_address.model.handle_bounce(mail.final_recipient)
|
51
|
+
true
|
52
|
+
else
|
53
|
+
false
|
54
|
+
end
|
55
|
+
elsif is_reply_email?
|
40
56
|
if reply_address = ReplyAddress.new_from_address(to)
|
41
57
|
reply_address.model.handle_reply(reply_address.user, stripped_body, files)
|
42
58
|
true
|
data/lib/replyr/engine.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Replyr
|
2
|
+
module HandleBounce
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Usage:
|
11
|
+
# class Comment < ActiveRecord::Base
|
12
|
+
# handle_bounce do |comment, email|
|
13
|
+
# # your custom code (e.g. mark email as invalid)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
def handle_bounce(*options, &block)
|
18
|
+
options = options.extract_options!
|
19
|
+
|
20
|
+
define_method :handle_bounce do |email|
|
21
|
+
block.call(self, email)
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method :bounce_address_object do
|
25
|
+
BounceAddress.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
define_method :bounce_address do
|
29
|
+
bounce_address_object.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|