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 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