replyr 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cc30e8d36b31046ac18c5ec4447a1ac051a512b2
4
- data.tar.gz: ed95db5d8aa64a306fdaad7818dd176ed3393402
3
+ metadata.gz: 09c0af70b27c7e54b91f2df46e1f397d57449109
4
+ data.tar.gz: b00e0b4df98e524e279235a580159b85b6714000
5
5
  SHA512:
6
- metadata.gz: ab02078a8475a15365ca3add436a3681ce988903eb718eb9eb9059ea424eae52d3a8071ac6b978cb161aa5397c507e09b421a18c0b85cade8c351f6d00804c46
7
- data.tar.gz: 49ef7f5734651590f4c6692bd9a046d2f85c97307006584454426c59127869b0ac64ab8c9b76ff4a0911ba59441a0f9e9ce0a25ce6988010837da633c1d4c078
6
+ metadata.gz: f2667bea0a35a65e7fde2635b636de4a99e33972e55425cd9be7f39835187add195525717296296cc54d630e4d55c394e511a9598a4a80dc3314937faba3f70a
7
+ data.tar.gz: ef21000d33e3b10ca64293290ca3699554c46d83f14653d4495e756bd8a78e689300bbc73ebae3b732f6216ffe0e0b26afa34da45d4cb767a1b6e2fda047868a
@@ -1,5 +1,9 @@
1
1
  ## v0.0.7
2
2
 
3
+ - Add handling of bounce emails
4
+
5
+ ## v0.0.7
6
+
3
7
  - Process the stripped body (signature removed)
4
8
 
5
9
  ## v0.0.6
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
- ## Installtion
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.host = "yourdomain.com"
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(to: user.email, reply_to: comment.reply_address_for_user(user))
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
- ## Start up
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 up the Mailman background worker with the following commands:
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.address_pattern do
38
- Replyr::ReplyEmail.process(message)
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"
@@ -1,9 +1,15 @@
1
1
  require 'mailman'
2
2
  require 'email_reply_parser/email_reply_parser'
3
3
  require "replyr/config"
4
- require "replyr/reply_email"
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
- def address_pattern
25
- "#{config.prefix}-%model_name%-%model_id%-%user_id%-%token%@#{config.host}"
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
@@ -1,17 +1,30 @@
1
1
  module Replyr
2
2
  class Config
3
- attr_accessor :prefix, :host, :secret, :user_class
3
+ attr_accessor :reply_prefix,
4
+ :reply_host,
5
+ :bounce_prefix,
6
+ :bounce_host,
7
+ :secret,
8
+ :user_class
4
9
 
5
- def prefix
6
- @prefix || "reply"
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 host
14
- @host || (raise RuntimeError, "Replyr.config.host is nil. Please set a host in an initializer.")
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 ReplyEmail
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.prefix)
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 is_reply_email?
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
@@ -8,6 +8,7 @@ module Replyr
8
8
 
9
9
  initializer "replyr.activerecord" do
10
10
  ActiveRecord::Base.send :extend, HandleReply::ClassMethods
11
+ ActiveRecord::Base.send :extend, HandleBounce::ClassMethods
11
12
  end
12
13
 
13
14
  end
@@ -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