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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/CONTRIBUTING.md +27 -0
  4. data/README.md +163 -17
  5. data/Rakefile +5 -4
  6. data/app/controllers/concerns/.keep +0 -0
  7. data/app/controllers/missive/application_controller.rb +4 -0
  8. data/app/controllers/missive/postmark/webhooks_controller.rb +92 -0
  9. data/app/helpers/missive/application_helper.rb +4 -0
  10. data/app/jobs/missive/application_job.rb +4 -0
  11. data/app/mailers/missive/application_mailer.rb +6 -0
  12. data/app/models/concerns/.keep +0 -0
  13. data/app/models/concerns/missive/suppressible.rb +26 -0
  14. data/app/models/concerns/missive/user.rb +10 -0
  15. data/app/models/concerns/missive/user_as_sender.rb +19 -0
  16. data/app/models/concerns/missive/user_as_subscriber.rb +23 -0
  17. data/app/models/missive/application_record.rb +5 -0
  18. data/app/models/missive/dispatch.rb +19 -0
  19. data/app/models/missive/list.rb +10 -0
  20. data/app/models/missive/message.rb +11 -0
  21. data/app/models/missive/sender.rb +10 -0
  22. data/app/models/missive/subscriber.rb +12 -0
  23. data/app/models/missive/subscription.rb +11 -0
  24. data/app/views/layouts/missive/application.html.erb +17 -0
  25. data/config/routes.rb +5 -0
  26. data/db/migrate/20251002000005_create_missive_subscribers.rb +12 -0
  27. data/db/migrate/20251004191513_create_missive_lists.rb +13 -0
  28. data/db/migrate/20251004193630_create_missive_messages.rb +13 -0
  29. data/db/migrate/20251004201105_create_missive_dispatches.rb +20 -0
  30. data/db/migrate/20251006214059_create_missive_subscriptions.rb +14 -0
  31. data/db/migrate/20251013205354_create_missive_senders.rb +17 -0
  32. data/lefthook.yml +13 -0
  33. data/lib/missive/engine.rb +7 -0
  34. data/lib/missive/railtie.rb +4 -0
  35. data/lib/missive/version.rb +1 -3
  36. data/lib/missive.rb +2 -4
  37. data/lib/tasks/missive_tasks.rake +4 -0
  38. metadata +89 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8f0fafdf0f75d8ed064f71eb41232a210e94a7cb628f7f1baf5ef143c21b9bf
4
- data.tar.gz: 1393cfff9b146e8b58e60e0be44241af4f5fddbf34a8058015187c487207ca73
3
+ metadata.gz: '08e4bc0cf32260c7519cd0ec0ab1b6f378b6e15f658ca559d593f5e2f69a51eb'
4
+ data.tar.gz: bb9b7dd6e243f03b4efb6cda902d542b0b7eec9e2e24c1e725f3237a1c39cfdb
5
5
  SHA512:
6
- metadata.gz: bead54c48d4b4a1114a420a4ec18c02b0a7238521a27528564c27424fcb91469afbde5544a118040d1e70e06b02a96250c756c76ae509456ba79c69595b11c6c
7
- data.tar.gz: 530f6996cd21ac2dd8a3d792bd436b6ff748f6e32064f9849091bf2740403610aaa4f853ffbe1505ee8c72ec2aaa7b44ed1d22d2d993e94d7ee706007ee938ac
6
+ metadata.gz: 61713bf0d79f835d91a739073efeae708dde7033d2f64aab5f11b696b69483f532c4e795eae4db8d13c256f25b05afdf413d8262fa9a9e2c5062902d41efc82a
7
+ data.tar.gz: 9441fbb01803f621f33252a5e1be5fa9ff94fd5c20b5355bfe3a1e0ba44824512404fef1c28378f8ac549ba3f162910a52dbc3e1af478caf9d858b8be3f34257
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.1] - 2025-10-14
4
+
5
+ - First release: base models, database structure
6
+
3
7
  ## [0.0.1.pre] - 2025-09-30
4
8
 
5
9
  - First commit
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
- TODO: Delete this and the text below, and describe your gem
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/missive`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Overview
6
6
 
7
- ## Installation
7
+ ### Features
8
8
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
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
- Install the gem and add to the application's Gemfile by executing:
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
55
+ bundle add missive
15
56
  ```
16
57
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
58
+ ### Configuration
18
59
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- ## Usage
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
- TODO: Write usage instructions here
76
+ ```rb
77
+ class User < ApplicationRecord
78
+ include Missive::UserAsSender
79
+ include Missive::UserAsSubscriber
80
+ end
81
+ ```
26
82
 
27
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
110
+ #### Manage subscriptions
30
111
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
112
+ ```rb
113
+ user = User.first
114
+ list = Missive::List.first
32
115
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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/[USERNAME]/missive/blob/main/CODE_OF_CONDUCT.md).
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/[USERNAME]/missive/blob/main/CODE_OF_CONDUCT.md).
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
- # frozen_string_literal: true
1
+ require "bundler/setup"
2
2
 
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
5
 
6
- Minitest::TestTask.create
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,4 @@
1
+ module Missive
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module Missive
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Missive
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Missive
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ 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,10 @@
1
+ module Missive
2
+ module User
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Missive::UserAsSender
7
+ include Missive::UserAsSubscriber
8
+ end
9
+ end
10
+ 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,5 @@
1
+ module Missive
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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 List < ApplicationRecord
3
+ belongs_to :sender
4
+ has_many :messages, dependent: :destroy
5
+ has_many :subscriptions, dependent: :destroy
6
+ has_many :subscribers, through: :subscriptions
7
+
8
+ validates :name, presence: true
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Missive
2
+ class Message < ApplicationRecord
3
+ belongs_to :sender
4
+ belongs_to :list, counter_cache: :messages_count
5
+ has_many :dispatches, dependent: :destroy
6
+
7
+ time_for_a_boolean :sent
8
+
9
+ validates :subject, presence: true
10
+ end
11
+ 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,5 @@
1
+ Missive::Engine.routes.draw do
2
+ namespace :postmark do
3
+ post "webhooks", to: "webhooks#receive"
4
+ end
5
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require "time_for_a_boolean"
2
+
3
+ module Missive
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Missive
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module Missive
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Missive
4
- VERSION = "0.0.1.pre"
2
+ VERSION = "0.0.1".freeze
5
3
  end
data/lib/missive.rb CHANGED
@@ -1,8 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "missive/version"
1
+ require "missive/version"
2
+ require "missive/engine"
4
3
 
5
4
  module Missive
6
- class Error < StandardError; end
7
5
  # Your code goes here...
8
6
  end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :missive do
3
+ # # Task goes here
4
+ # end
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.pre
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