missive 0.0.1.pre → 0.0.1
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/CONTRIBUTING.md +27 -0
- data/README.md +163 -17
- data/Rakefile +5 -4
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/missive/application_controller.rb +4 -0
- data/app/controllers/missive/postmark/webhooks_controller.rb +92 -0
- data/app/helpers/missive/application_helper.rb +4 -0
- data/app/jobs/missive/application_job.rb +4 -0
- data/app/mailers/missive/application_mailer.rb +6 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/missive/suppressible.rb +26 -0
- data/app/models/concerns/missive/user.rb +10 -0
- data/app/models/concerns/missive/user_as_sender.rb +19 -0
- data/app/models/concerns/missive/user_as_subscriber.rb +23 -0
- data/app/models/missive/application_record.rb +5 -0
- data/app/models/missive/dispatch.rb +19 -0
- data/app/models/missive/list.rb +10 -0
- data/app/models/missive/message.rb +11 -0
- data/app/models/missive/sender.rb +10 -0
- data/app/models/missive/subscriber.rb +12 -0
- data/app/models/missive/subscription.rb +11 -0
- data/app/views/layouts/missive/application.html.erb +17 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20251002000005_create_missive_subscribers.rb +12 -0
- data/db/migrate/20251004191513_create_missive_lists.rb +13 -0
- data/db/migrate/20251004193630_create_missive_messages.rb +13 -0
- data/db/migrate/20251004201105_create_missive_dispatches.rb +20 -0
- data/db/migrate/20251006214059_create_missive_subscriptions.rb +14 -0
- data/db/migrate/20251013205354_create_missive_senders.rb +17 -0
- data/lefthook.yml +13 -0
- data/lib/missive/engine.rb +7 -0
- data/lib/missive/railtie.rb +4 -0
- data/lib/missive/version.rb +1 -3
- data/lib/missive.rb +2 -4
- data/lib/tasks/missive_tasks.rake +4 -0
- metadata +89 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08e4bc0cf32260c7519cd0ec0ab1b6f378b6e15f658ca559d593f5e2f69a51eb'
|
4
|
+
data.tar.gz: bb9b7dd6e243f03b4efb6cda902d542b0b7eec9e2e24c1e725f3237a1c39cfdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61713bf0d79f835d91a739073efeae708dde7033d2f64aab5f11b696b69483f532c4e795eae4db8d13c256f25b05afdf413d8262fa9a9e2c5062902d41efc82a
|
7
|
+
data.tar.gz: 9441fbb01803f621f33252a5e1be5fa9ff94fd5c20b5355bfe3a1e0ba44824512404fef1c28378f8ac549ba3f162910a52dbc3e1af478caf9d858b8be3f34257
|
data/CHANGELOG.md
CHANGED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Spone/missive. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Spone/missive/blob/main/CODE_OF_CONDUCT.md).
|
4
|
+
|
5
|
+
## Testing
|
6
|
+
|
7
|
+
Run the test suite with:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
bin/rails test
|
11
|
+
```
|
12
|
+
|
13
|
+
## Get started
|
14
|
+
|
15
|
+
- Fork the repository
|
16
|
+
- Run `bin/setup` to install dependencies
|
17
|
+
- Create your feature branch (`git checkout -b my-new-feature`)
|
18
|
+
- Make your changes
|
19
|
+
- Run the tests `bin/rails test`
|
20
|
+
- Commit your change, push the branch and create a pull request
|
21
|
+
|
22
|
+
## Release
|
23
|
+
|
24
|
+
- Update the version number in `version.rb`
|
25
|
+
- Run `bundle exec rake release`, which will create a git tag for the version
|
26
|
+
- Push git commits and the created tag
|
27
|
+
- Push the `.gem` file to [rubygems.org](https://rubygems.org)
|
data/README.md
CHANGED
@@ -1,38 +1,184 @@
|
|
1
1
|
# Missive
|
2
2
|
|
3
|
-
|
3
|
+
A lightweight Rails toolkit for building newsletter features. Missive provides the primitives for managing newsletters and subscribers, with [Postmark](https://postmarkapp.com/) handling delivery.
|
4
4
|
|
5
|
-
|
5
|
+
## Overview
|
6
6
|
|
7
|
-
|
7
|
+
### Features
|
8
8
|
|
9
|
-
|
9
|
+
- **Newsletter Management**: Create and organize newsletters within your Rails application
|
10
|
+
- **Postmark Integration**: Leverage Postmark's reliable email delivery service
|
11
|
+
- **Rails Native**: Designed to work seamlessly with Rails conventions and ActionMailer
|
12
|
+
- **Subscriber Management**: Handle your newsletter subscriber lists
|
10
13
|
|
11
|
-
|
14
|
+
### Scope
|
15
|
+
|
16
|
+
#### Goals
|
17
|
+
|
18
|
+
- Rely on Postmark as much as possible, integrate with webhooks, send through Postmark API (not SMTP).
|
19
|
+
- Provide a way to compose messages that include content from the host app models.
|
20
|
+
|
21
|
+
#### Non-goals
|
22
|
+
|
23
|
+
- Integrate with other sending services.
|
24
|
+
- Send transactional emails or email sequences: Missive focuses on newsletters.
|
25
|
+
- Provide ready-made subscription forms: subscription management is the responsibility of the host app.
|
26
|
+
- Multi-tenancy: Missive is designed for a single Rails app and domain.
|
27
|
+
|
28
|
+
## Concepts
|
29
|
+
|
30
|
+
### Models
|
31
|
+
|
32
|
+
- `Sender` is an identity used to send messages, optionally associated with a `User` from the host app. It has a corresponding [Sender Signature](https://postmarkapp.com/developer/api/signatures-api) in Postmark.
|
33
|
+
- `Subscriber` is a person who have opted in to receiving emails or have been added manually, optionally associated with a `User` from the host app.
|
34
|
+
- `Subscription` is the relation between a subscriber and a list.
|
35
|
+
- `List` is a list of subscribers. It uses a specific [Message Stream](https://postmarkapp.com/developer/api/message-streams-api) to send messages through Postmark.
|
36
|
+
- `Message` is an email sent to subscribers of a given list.
|
37
|
+
- `Dispatch` is the relation between a subscriber and a message (ie. when a `Message` is sent, it's dispatched to all subscribers). It's called [Email](https://postmarkapp.com/developer/api/email-api) in Postmark.
|
38
|
+
|
39
|
+
## Requirements
|
40
|
+
|
41
|
+
- Rails 8.0 or higher
|
42
|
+
- A Postmark account with API credentials
|
43
|
+
|
44
|
+
### Dependencies
|
45
|
+
|
46
|
+
- Official [postmark](https://github.com/activecampaign/postmark-gem) gem
|
47
|
+
- [time_for_a_boolean](https://github.com/calebhearth/time_for_a_boolean) to back boolean concepts (`sent`, `delivered`, `open`, ...) with timestamps
|
48
|
+
- [rails-pattern_matching](https://github.com/kddnewton/rails-pattern_matching) to use pattern matching when processing incoming webhooks
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Setup
|
12
53
|
|
13
54
|
```bash
|
14
|
-
bundle add
|
55
|
+
bundle add missive
|
15
56
|
```
|
16
57
|
|
17
|
-
|
58
|
+
### Configuration
|
18
59
|
|
19
|
-
|
20
|
-
|
60
|
+
Missive uses the same configuration as `postmark-rails`. Please follow the [`postmark-rails` configuration instructions](https://github.com/ActiveCampaign/postmark-rails?tab=readme-ov-file#installation) to set up your Postmark API credentials.
|
61
|
+
|
62
|
+
### Quick start
|
63
|
+
|
64
|
+
#### Connect the host app `User` model (optional)
|
65
|
+
|
66
|
+
The host app `User` can be associated to a `Missive::Subscriber` and/or a `Missive::Sender`, using the `Missive::User` concern.
|
67
|
+
|
68
|
+
```rb
|
69
|
+
class User < ApplicationRecord
|
70
|
+
include Missive::User
|
71
|
+
end
|
21
72
|
```
|
22
73
|
|
23
|
-
|
74
|
+
The concerns can also be included separately, which is useful if `User` needs to be implemented as a `Sender` or `Subscriber` only.
|
24
75
|
|
25
|
-
|
76
|
+
```rb
|
77
|
+
class User < ApplicationRecord
|
78
|
+
include Missive::UserAsSender
|
79
|
+
include Missive::UserAsSubscriber
|
80
|
+
end
|
81
|
+
```
|
26
82
|
|
27
|
-
|
83
|
+
This is equivalent to:
|
84
|
+
|
85
|
+
```rb
|
86
|
+
class User < ApplicationRecord
|
87
|
+
# Missive::UserAsSender
|
88
|
+
has_one :sender # ...
|
89
|
+
has_many :sent_dispatches # ...
|
90
|
+
has_many :sent_lists # ...
|
91
|
+
has_many :sent_messages # ...
|
92
|
+
|
93
|
+
def init_sender(attributes = {});
|
94
|
+
# ...
|
95
|
+
end
|
96
|
+
|
97
|
+
# Missive::UserAsSubscriber
|
98
|
+
has_one :subscriber # ...
|
99
|
+
has_many :dispatches # ...
|
100
|
+
has_many :subscriptions # ...
|
101
|
+
has_many :subscribed_lists # ...
|
102
|
+
has_many :unsubscribed_lists # ...
|
103
|
+
|
104
|
+
def init_subscriber(attributes = {})
|
105
|
+
# ...
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
28
109
|
|
29
|
-
|
110
|
+
#### Manage subscriptions
|
30
111
|
|
31
|
-
|
112
|
+
```rb
|
113
|
+
user = User.first
|
114
|
+
list = Missive::List.first
|
32
115
|
|
33
|
-
|
116
|
+
# Make sure the User has an associated Missive::Subscriber:
|
117
|
+
# - if one exists with the same email, associate it
|
118
|
+
# - else create a new subscriber with the same email
|
119
|
+
user.init_subscriber
|
34
120
|
|
35
|
-
|
121
|
+
# List the subscriptions
|
122
|
+
user.subscriptions # returns a `Missive::Subscription` collection
|
123
|
+
|
124
|
+
# List the (un)subscribed lists
|
125
|
+
user.subscribed_lists # returns a `Missive::List` collection
|
126
|
+
user.unsubscribed_lists # returns a `Missive::List` collection
|
127
|
+
|
128
|
+
# Subscribe to an existing Missive::List
|
129
|
+
user.subscriber.subscriptions.create!(list:)
|
130
|
+
|
131
|
+
# Unsubscribe from the list
|
132
|
+
user.subscriptions.find_by(list:).suppress!(reason: :manual_suppression)
|
133
|
+
```
|
134
|
+
|
135
|
+
#### Manage senders
|
136
|
+
|
137
|
+
```rb
|
138
|
+
user = User.where(admin: true).first
|
139
|
+
list = Missive::List.first
|
140
|
+
|
141
|
+
# Make sure the User has an associated Missive::Sender:
|
142
|
+
# - if one exists with the same email, associate it
|
143
|
+
# - else create a new sender with the same email
|
144
|
+
# then assign them the provided name
|
145
|
+
user.init_sender(name: user.full_name)
|
146
|
+
|
147
|
+
# Make them the default sender for a list
|
148
|
+
user.sent_lists << list
|
149
|
+
```
|
150
|
+
|
151
|
+
#### Manage lists
|
152
|
+
|
153
|
+
```rb
|
154
|
+
# Create a new list
|
155
|
+
list = Missive::List.create!(name: "My newsletter")
|
156
|
+
|
157
|
+
# Choose a specific Message Stream to send messages for this list
|
158
|
+
list.update!(postmark_message_stream_id: "bulk")
|
159
|
+
|
160
|
+
# Get available lists
|
161
|
+
Missive::List.all
|
162
|
+
|
163
|
+
# Get list stats
|
164
|
+
list.subscriptions_count # how many people subscribe or unsubscribe to this list?
|
165
|
+
list.messages_count # how many messages have been created in this list?
|
166
|
+
```
|
167
|
+
|
168
|
+
> [!WARNING]
|
169
|
+
> **Everything below is currently being developed and is not yet ready for use.**
|
170
|
+
|
171
|
+
#### Manage messages
|
172
|
+
|
173
|
+
```rb
|
174
|
+
# Create a new message in a list
|
175
|
+
list.create_message!(subject: "Hello world!")
|
176
|
+
|
177
|
+
# TODO: add message content
|
178
|
+
|
179
|
+
# Send the message to the list
|
180
|
+
message.send!
|
181
|
+
```
|
36
182
|
|
37
183
|
## License
|
38
184
|
|
@@ -40,4 +186,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
186
|
|
41
187
|
## Code of Conduct
|
42
188
|
|
43
|
-
Everyone interacting in the Missive project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
189
|
+
Everyone interacting in the Missive project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Spone/missive/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
|
1
|
+
require "bundler/setup"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
5
|
|
6
|
-
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
7
|
|
8
8
|
require "standard/rake"
|
9
|
+
require "bundler/gem_tasks"
|
9
10
|
|
10
11
|
task default: %i[test standard]
|
File without changes
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Missive
|
2
|
+
class Postmark::WebhooksController < ApplicationController
|
3
|
+
skip_forgery_protection
|
4
|
+
|
5
|
+
class RecipientNotMatching < StandardError; end
|
6
|
+
|
7
|
+
before_action :verify_webhook
|
8
|
+
before_action :set_payload, only: :receive
|
9
|
+
|
10
|
+
rescue_from RecipientNotMatching, with: :handle_bad_request
|
11
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
|
12
|
+
rescue_from NoMatchingPatternError, with: :handle_no_matching_pattern
|
13
|
+
|
14
|
+
def receive
|
15
|
+
case @payload
|
16
|
+
in {RecordType: "Open", ReceivedAt: opened_at}
|
17
|
+
set_subscriber! && set_dispatch! && check_dispatch_recipient!
|
18
|
+
@dispatch.update!(opened_at:)
|
19
|
+
in {RecordType: "Delivery", DeliveredAt: delivered_at}
|
20
|
+
set_subscriber! && set_dispatch! && check_dispatch_recipient!
|
21
|
+
@dispatch.update!(delivered_at:)
|
22
|
+
in {RecordType: "SubscriptionChange", ChangedAt: suppressed_at, SuppressSending: true, SuppressionReason: suppression_reason}
|
23
|
+
set_subscriber! && set_dispatch && set_subscription
|
24
|
+
reason = suppression_reason.underscore
|
25
|
+
@subscriber.suppress!(reason:)
|
26
|
+
@dispatch&.suppress!(reason:)
|
27
|
+
@subscription&.suppress!(reason:)
|
28
|
+
in {RecordType: "SubscriptionChange", SuppressSending: false}
|
29
|
+
set_subscriber! && set_dispatch && set_subscription
|
30
|
+
@subscriber.unsuppress!
|
31
|
+
@dispatch&.unsuppress!
|
32
|
+
@subscription&.unsuppress!
|
33
|
+
end
|
34
|
+
|
35
|
+
head :ok
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def verify_webhook
|
41
|
+
secret_header = request.headers["HTTP_X_POSTMARK_SECRET"]
|
42
|
+
|
43
|
+
render plain: "Cannot verify webhook", status: :unauthorized if secret_header != webhooks_secret
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_payload
|
47
|
+
@payload = JSON.parse(request.body.read).with_indifferent_access
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_dispatch
|
51
|
+
@dispatch = Dispatch.includes(:subscriber, :list).find_by(postmark_message_id: @payload["MessageID"])
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_dispatch!
|
55
|
+
@dispatch = Dispatch.includes(:subscriber, :list).find_by!(postmark_message_id: @payload["MessageID"])
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_subscriber!
|
59
|
+
@subscriber = Subscriber.find_by!(email: @payload["Recipient"])
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_subscription
|
63
|
+
return if @dispatch.nil?
|
64
|
+
|
65
|
+
list = @dispatch.list
|
66
|
+
@subscription = Subscription.find_by(list:, subscriber: @subscriber)
|
67
|
+
end
|
68
|
+
|
69
|
+
def check_dispatch_recipient!
|
70
|
+
return unless @dispatch.subscriber != @subscriber
|
71
|
+
|
72
|
+
raise RecipientNotMatching,
|
73
|
+
"Dispatch subscriber #{@dispatch.subscriber.email} does not match payload recipient #{@payload["Recipient"]}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def webhooks_secret
|
77
|
+
Rails.application.credentials.postmark.webhooks_secret
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_bad_request(e)
|
81
|
+
render plain: e.message, status: :bad_request
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_not_found(e)
|
85
|
+
render plain: "#{e.model} not found", status: :not_found
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_no_matching_pattern
|
89
|
+
render plain: "Webhook payload not supported", status: :unprocessable_entity
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
File without changes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Missive
|
2
|
+
module Suppressible
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
time_for_a_boolean :suppressed
|
7
|
+
|
8
|
+
enum :suppression_reason, %i[hard_bounce spam_complaint manual_suppression]
|
9
|
+
|
10
|
+
scope :suppressed, -> { where.not(suppressed_at: nil) }
|
11
|
+
scope :not_suppressed, -> { where(suppressed_at: nil) }
|
12
|
+
|
13
|
+
validates :suppression_reason, presence: true, if: :suppressed?
|
14
|
+
|
15
|
+
def suppress!(reason:)
|
16
|
+
self.suppression_reason = reason
|
17
|
+
suppressed!
|
18
|
+
save!
|
19
|
+
end
|
20
|
+
|
21
|
+
def unsuppress!
|
22
|
+
update!(suppressed_at: nil, suppression_reason: nil)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Missive
|
2
|
+
module UserAsSender
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_one :sender, class_name: "Missive::Sender", dependent: :nullify
|
7
|
+
has_many :sent_dispatches, class_name: "Missive::Dispatch", through: :sender, source: :dispatches
|
8
|
+
has_many :sent_lists, class_name: "Missive::List", through: :sender, source: :lists
|
9
|
+
has_many :sent_messages, class_name: "Missive::Message", through: :sender, source: :messages
|
10
|
+
|
11
|
+
def init_sender(attributes = {})
|
12
|
+
self.sender = Missive::Sender.find_or_initialize_by(email:)
|
13
|
+
sender.assign_attributes(attributes)
|
14
|
+
sender.save!
|
15
|
+
sender
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Missive
|
2
|
+
module UserAsSubscriber
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_one :subscriber, class_name: "Missive::Subscriber", dependent: :destroy
|
7
|
+
has_many :dispatches, class_name: "Missive::Dispatch", through: :subscriber
|
8
|
+
has_many :subscriptions, class_name: "Missive::Subscription", through: :subscriber
|
9
|
+
has_many :subscribed_lists, class_name: "Missive::List", through: :subscriber, source: :lists
|
10
|
+
has_many :unsubscribed_lists, -> { where.not(missive_subscriptions: {suppressed_at: nil}) },
|
11
|
+
class_name: "Missive::List",
|
12
|
+
through: :subscriber,
|
13
|
+
source: :lists
|
14
|
+
|
15
|
+
def init_subscriber(attributes = {})
|
16
|
+
self.subscriber = Missive::Subscriber.find_or_initialize_by(email:)
|
17
|
+
subscriber.assign_attributes(attributes)
|
18
|
+
subscriber.save!
|
19
|
+
subscriber
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Missive
|
2
|
+
class Dispatch < ApplicationRecord
|
3
|
+
include Suppressible
|
4
|
+
|
5
|
+
belongs_to :sender
|
6
|
+
belongs_to :subscriber
|
7
|
+
belongs_to :message, counter_cache: :dispatches_count
|
8
|
+
has_one :list, through: :message
|
9
|
+
has_one :subscription, ->(dispatch) { where(list: dispatch.list) }, through: :subscriber, source: :subscriptions
|
10
|
+
|
11
|
+
time_for_a_boolean :sent
|
12
|
+
time_for_a_boolean :delivered
|
13
|
+
time_for_a_boolean :opened
|
14
|
+
time_for_a_boolean :clicked
|
15
|
+
|
16
|
+
validates :message, uniqueness: {scope: :subscriber,
|
17
|
+
message: "should be dispatched once per subscriber"}
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Missive
|
2
|
+
class Sender < ApplicationRecord
|
3
|
+
belongs_to :user, class_name: "::User", optional: true
|
4
|
+
has_many :dispatches, dependent: :restrict_with_exception
|
5
|
+
has_many :lists, dependent: :restrict_with_exception
|
6
|
+
has_many :messages, dependent: :restrict_with_exception
|
7
|
+
|
8
|
+
validates :email, presence: true
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Missive
|
2
|
+
class Subscriber < ApplicationRecord
|
3
|
+
include Suppressible
|
4
|
+
|
5
|
+
belongs_to :user, class_name: "::User", optional: true
|
6
|
+
has_many :dispatches, dependent: :destroy
|
7
|
+
has_many :subscriptions, dependent: :destroy
|
8
|
+
has_many :lists, through: :subscriptions
|
9
|
+
|
10
|
+
validates :email, presence: true
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Missive
|
2
|
+
class Subscription < ApplicationRecord
|
3
|
+
include Suppressible
|
4
|
+
|
5
|
+
belongs_to :subscriber
|
6
|
+
belongs_to :list, counter_cache: :subscriptions_count
|
7
|
+
|
8
|
+
validates :list, uniqueness: {scope: :subscriber,
|
9
|
+
message: "should be subscribed once per subscriber"}
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Missive</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<%= stylesheet_link_tag "missive/application", media: "all" %>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
|
14
|
+
<%= yield %>
|
15
|
+
|
16
|
+
</body>
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateMissiveSubscribers < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_subscribers do |t|
|
4
|
+
t.string :email, null: false
|
5
|
+
t.timestamp :suppressed_at
|
6
|
+
t.integer :suppression_reason
|
7
|
+
t.references :user, foreign_key: true
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateMissiveLists < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_lists do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.integer :subscriptions_count, default: 0
|
6
|
+
t.integer :messages_count, default: 0
|
7
|
+
t.timestamp :last_message_sent_at
|
8
|
+
t.string :postmark_message_stream_id
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateMissiveMessages < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_messages do |t|
|
4
|
+
t.string :subject, null: false
|
5
|
+
t.integer :dispatches_count, default: 0
|
6
|
+
t.references :list, null: false, foreign_key: {to_table: "missive_lists"}
|
7
|
+
t.string :postmark_message_stream_id
|
8
|
+
t.timestamp :sent_at
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateMissiveDispatches < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_dispatches do |t|
|
4
|
+
t.references :subscriber, null: false, foreign_key: {to_table: "missive_subscribers"}
|
5
|
+
t.references :message, null: false, foreign_key: {to_table: "missive_messages"}
|
6
|
+
t.string :postmark_message_stream_id
|
7
|
+
t.string :postmark_message_id
|
8
|
+
t.timestamp :sent_at
|
9
|
+
t.timestamp :delivered_at
|
10
|
+
t.timestamp :opened_at
|
11
|
+
t.timestamp :clicked_at
|
12
|
+
t.timestamp :suppressed_at
|
13
|
+
t.integer :suppression_reason
|
14
|
+
|
15
|
+
t.index [:subscriber_id, :message_id], unique: true
|
16
|
+
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateMissiveSubscriptions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_subscriptions do |t|
|
4
|
+
t.references :subscriber, null: false, foreign_key: {to_table: "missive_subscribers"}
|
5
|
+
t.references :list, null: false, foreign_key: {to_table: "missive_lists"}
|
6
|
+
t.timestamp :suppressed_at
|
7
|
+
t.integer :suppression_reason
|
8
|
+
|
9
|
+
t.index [:subscriber_id, :list_id], unique: true
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateMissiveSenders < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :missive_senders do |t|
|
4
|
+
t.string :email, null: false
|
5
|
+
t.string :name
|
6
|
+
t.string :reply_to_email
|
7
|
+
t.integer :postmark_sender_signature_id
|
8
|
+
t.references :user, foreign_key: false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_reference :missive_dispatches, :sender, null: false, foreign_key: {to_table: "missive_senders"}
|
14
|
+
add_reference :missive_lists, :sender, null: false, foreign_key: {to_table: "missive_senders"}
|
15
|
+
add_reference :missive_messages, :sender, null: false, foreign_key: {to_table: "missive_senders"}
|
16
|
+
end
|
17
|
+
end
|
data/lefthook.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# EXAMPLE USAGE:
|
2
|
+
#
|
3
|
+
# Refer for explanation to following link:
|
4
|
+
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
|
5
|
+
|
6
|
+
pre-commit:
|
7
|
+
parallel: true
|
8
|
+
commands:
|
9
|
+
standard:
|
10
|
+
tags: ruby style
|
11
|
+
glob: "**/*.rb"
|
12
|
+
run: bundle exec rake standard:fix {staged_files}
|
13
|
+
stage_fixed: true
|
data/lib/missive/version.rb
CHANGED
data/lib/missive.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,70 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: missive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hans Lemuet
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: postmark-rails
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.22.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.22.1
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: time_for_a_boolean
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.2.1
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.2.1
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rails-pattern_matching
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.3.0
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.3.0
|
12
68
|
description: 'Missive provides primitives to build your own custom newsletter: lists,
|
13
69
|
subscribers, content editor, tracking.'
|
14
70
|
email:
|
@@ -20,11 +76,42 @@ files:
|
|
20
76
|
- ".standard.yml"
|
21
77
|
- CHANGELOG.md
|
22
78
|
- CODE_OF_CONDUCT.md
|
79
|
+
- CONTRIBUTING.md
|
23
80
|
- LICENSE.txt
|
24
81
|
- README.md
|
25
82
|
- Rakefile
|
83
|
+
- app/controllers/concerns/.keep
|
84
|
+
- app/controllers/missive/application_controller.rb
|
85
|
+
- app/controllers/missive/postmark/webhooks_controller.rb
|
86
|
+
- app/helpers/missive/application_helper.rb
|
87
|
+
- app/jobs/missive/application_job.rb
|
88
|
+
- app/mailers/missive/application_mailer.rb
|
89
|
+
- app/models/concerns/.keep
|
90
|
+
- app/models/concerns/missive/suppressible.rb
|
91
|
+
- app/models/concerns/missive/user.rb
|
92
|
+
- app/models/concerns/missive/user_as_sender.rb
|
93
|
+
- app/models/concerns/missive/user_as_subscriber.rb
|
94
|
+
- app/models/missive/application_record.rb
|
95
|
+
- app/models/missive/dispatch.rb
|
96
|
+
- app/models/missive/list.rb
|
97
|
+
- app/models/missive/message.rb
|
98
|
+
- app/models/missive/sender.rb
|
99
|
+
- app/models/missive/subscriber.rb
|
100
|
+
- app/models/missive/subscription.rb
|
101
|
+
- app/views/layouts/missive/application.html.erb
|
102
|
+
- config/routes.rb
|
103
|
+
- db/migrate/20251002000005_create_missive_subscribers.rb
|
104
|
+
- db/migrate/20251004191513_create_missive_lists.rb
|
105
|
+
- db/migrate/20251004193630_create_missive_messages.rb
|
106
|
+
- db/migrate/20251004201105_create_missive_dispatches.rb
|
107
|
+
- db/migrate/20251006214059_create_missive_subscriptions.rb
|
108
|
+
- db/migrate/20251013205354_create_missive_senders.rb
|
109
|
+
- lefthook.yml
|
26
110
|
- lib/missive.rb
|
111
|
+
- lib/missive/engine.rb
|
112
|
+
- lib/missive/railtie.rb
|
27
113
|
- lib/missive/version.rb
|
114
|
+
- lib/tasks/missive_tasks.rake
|
28
115
|
homepage: https://github.com/Spone/missive
|
29
116
|
licenses:
|
30
117
|
- MIT
|