nuntius 1.4.15 → 1.5.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -1
  3. data/app/controllers/nuntius/admin/campaigns_controller.rb +1 -1
  4. data/app/controllers/nuntius/admin/lists/subscribers_controller.rb +24 -0
  5. data/app/controllers/nuntius/subscribers_controller.rb +37 -0
  6. data/app/drops/nuntius/campaign_drop.rb +1 -1
  7. data/app/drops/nuntius/message_drop.rb +1 -1
  8. data/app/drops/nuntius/subscriber_drop.rb +9 -1
  9. data/app/helpers/nuntius/application_helper.rb +1 -0
  10. data/app/jobs/nuntius/campaign_publish_job.rb +9 -0
  11. data/app/jobs/nuntius/import_subscribers_job.rb +36 -0
  12. data/app/jobs/nuntius/purge_message_job.rb +1 -1
  13. data/app/jobs/nuntius/transport_delivery_job.rb +1 -1
  14. data/app/messengers/nuntius/message_messenger.rb +21 -0
  15. data/app/models/nuntius/message.rb +36 -24
  16. data/app/providers/nuntius/apnotic_push_provider.rb +3 -3
  17. data/app/providers/nuntius/firebase_push_provider.rb +3 -3
  18. data/app/providers/nuntius/message_bird_sms_provider.rb +3 -3
  19. data/app/providers/nuntius/slack_slack_provider.rb +3 -3
  20. data/app/providers/nuntius/smstools_sms_provider.rb +1 -1
  21. data/app/providers/nuntius/smtp_mail_provider.rb +7 -7
  22. data/app/providers/nuntius/teams_teams_provider.rb +3 -3
  23. data/app/providers/nuntius/twilio_sms_provider.rb +2 -2
  24. data/app/providers/nuntius/twilio_voice_provider.rb +2 -2
  25. data/app/services/nuntius/aws_sns_processor_service.rb +3 -3
  26. data/app/tables/nuntius/messages_table.rb +1 -1
  27. data/app/views/nuntius/admin/campaigns/edit.html.slim +6 -4
  28. data/app/views/nuntius/admin/lists/subscribers/import.html.slim +15 -0
  29. data/app/views/nuntius/admin/messages/show.html.slim +1 -1
  30. data/app/views/nuntius/subscribers/edit.html.slim +20 -0
  31. data/config/locales/en.yml +15 -0
  32. data/config/locales/nl.yml +17 -1
  33. data/config/routes.rb +6 -1
  34. data/db/migrate/20260324140324_add_publish_at_to_campaign.rb +5 -0
  35. data/db/migrate/20260324150714_rename_status_to_state.rb +5 -0
  36. data/lib/nuntius/version.rb +1 -1
  37. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fe72b9374b7311ad55ccebb5f2c8c822490d4a35e21ab55f6336293dbfd97df
4
- data.tar.gz: 954511ce5295466775f5f3f5a955189bd1e045c8a442cbce802bb78b5ebe8fe1
3
+ metadata.gz: 25dbff01510e68a23dbd0bf046aefd97cae3ad2633c88bb8cd702d42639b6a50
4
+ data.tar.gz: 4c87bfd9ba23075725696538ee1fb1e1d9ba0661e07704b23ac3f75fbfb2f00c
5
5
  SHA512:
6
- metadata.gz: ad5df8d35bb3f0289a1ca8f59946b346670e98bc5ad9267e052b7005bb030c904287415d034c030ac6fd9a6f99a29168aee5d0d4468c9775d5924f52b950d09a
7
- data.tar.gz: a8a11a0598079d133ab31b7343ee1444a198245d735e583295aecb6ad9d7a66f729029250feabc96f1a8e36035d71cd37d9cdda2b836a60bebb0ebd4dd8d8c77
6
+ metadata.gz: 47904897c1e9c09e5a1dab2bb82a979bc312f401c7d69766947e275729a30be0cb41f923614b2c558dd24edec2daacbe5b1c4fd2a0b22173aa7a5e68feb0a9c2
7
+ data.tar.gz: a307808cc8cac2b8b8077ea9c78f3da025b830367a4b5c8ebc8eaacacd0e985f4fe0e90c866e348246c02725ef0b6f5c0a9a3440a854bd07a9291e6fbe0082a3
data/README.md CHANGED
@@ -357,10 +357,17 @@ class BarMessageBox < Nuntius::BaseMessageBox
357
357
  route to: :process
358
358
 
359
359
  def process
360
- puts message.status # message is Nuntius's inbound message.
360
+ puts message.state # message is Nuntius's inbound message.
361
361
  puts mail.to # mail gives you the Mail representation, when it's a mail (transport)
362
362
  end
363
363
  end
364
364
  ```
365
365
 
366
366
  Add `Nuntius::RetrieveMailJob` to your cron.
367
+
368
+ ## Campaigns
369
+
370
+ Campaigns allows you to bulk send many mails to subscribers.
371
+ We support multiple lists with many subscribers.
372
+
373
+ You can manually publish the campaign or have Nuntius publish the campaign after a certain date/time. If you want to have Nuntius publish it for you, add `Nuntius::CampaignPublishJob` to your cron.
@@ -51,7 +51,7 @@ module Nuntius
51
51
  def campaign_params
52
52
  return unless params[:campaign]
53
53
 
54
- params.require(:campaign).permit(:name, :transport, :layout_id, :list_id, :from, :subject, :text, :html, :metadata_yaml, :open_tracking, :link_tracking)
54
+ params.require(:campaign).permit(:name, :transport, :layout_id, :list_id, :from, :subject, :text, :html, :metadata_yaml, :open_tracking, :link_tracking, :publish_at)
55
55
  end
56
56
  end
57
57
  end
@@ -12,6 +12,26 @@ module Nuntius
12
12
  @subscribers = @list.subscribers.all
13
13
  end
14
14
 
15
+ def import
16
+ return unless request.post?
17
+
18
+ file = params[:file]
19
+ if file.blank?
20
+ Signum.error(Current.user, text: t(".no_file"))
21
+ redirect_to import_admin_list_subscribers_path(@list) and return
22
+ end
23
+
24
+ blob = ActiveStorage::Blob.create_and_upload!(
25
+ io: file,
26
+ filename: file.original_filename,
27
+ content_type: "text/csv"
28
+ )
29
+ ImportSubscribersJob.perform_later(@list, blob, Current.user)
30
+
31
+ Signum.info(Current.user, text: t(".queued"))
32
+ redirect_to nuntius.admin_list_path(@list)
33
+ end
34
+
15
35
  def new
16
36
  @subscriber = @list.subscribers.new
17
37
  render :edit
@@ -41,6 +61,10 @@ module Nuntius
41
61
  params.require(:subscriber).permit(:first_name, :last_name, :email, :phone_number, :metadata_yaml, tags: [])
42
62
  end
43
63
 
64
+ def import_params
65
+ params.permit(:file)
66
+ end
67
+
44
68
  def set_objects
45
69
  @list = List.find(params[:list_id])
46
70
  end
@@ -8,6 +8,37 @@ module Nuntius
8
8
  skip_before_action :verify_authenticity_token, only: :unsubscribe
9
9
  layout "empty"
10
10
 
11
+ def new
12
+ @subscriber = Nuntius::Subscriber.new(list: Nuntius::List.find(params[:list_id]))
13
+ render :edit
14
+ end
15
+
16
+ def create
17
+ @subscriber = Nuntius::Subscriber.new(subscriber_params)
18
+ if @subscriber.save
19
+ Signum.success(request.session.id, text: "You have been subscribed.")
20
+ redirect_to nuntius.edit_subscriber_path(@subscriber), status: :see_other
21
+ else
22
+ render :edit, status: :unprocessable_entity
23
+ end
24
+ end
25
+
26
+ def edit
27
+ @subscriber = Nuntius::Subscriber.find(params[:id])
28
+ @list = @subscriber.list
29
+ end
30
+
31
+ def update
32
+ @subscriber = Nuntius::Subscriber.find(params[:id])
33
+ @list = @subscriber.list
34
+ if @subscriber.update(subscriber_params)
35
+ Signum.success(request.session.id, text: "Subscription has been updated.")
36
+ redirect_to nuntius.edit_subscriber_path(@subscriber), status: :see_other
37
+ else
38
+ render :edit, status: :unprocessable_entity
39
+ end
40
+ end
41
+
11
42
  def show
12
43
  @subscriber = Nuntius::Subscriber.find_by(id: params[:id])
13
44
  if @subscriber
@@ -32,5 +63,11 @@ module Nuntius
32
63
  Signum.success(request.session.id, text: "Subscription has been removed.")
33
64
  redirect_to nuntius.subscriber_path(@subscriber), status: :see_other
34
65
  end
66
+
67
+ private
68
+
69
+ def subscriber_params
70
+ params.require(:subscriber).permit(:email, :list_id, :first_name, :last_name, :phone_number)
71
+ end
35
72
  end
36
73
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Nuntius
4
4
  class CampaignDrop < ApplicationDrop
5
- delegate :id, :metadata, to: :@object
5
+ delegate :id, :metadata, :name, :transport, to: :@object
6
6
  end
7
7
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Nuntius
4
4
  class MessageDrop < ApplicationDrop
5
- delegate :id, :from, :to, :subject, :html, :text, to: :@object
5
+ delegate :id, :from, :to, :subject, :html, :text, :template, :campaign, to: :@object
6
6
 
7
7
  def base_url
8
8
  Nuntius::Engine.routes.url_helpers.message_url(@object.id, host: host)
@@ -2,6 +2,14 @@
2
2
 
3
3
  module Nuntius
4
4
  class SubscriberDrop < ApplicationDrop
5
- delegate :id, :first_name, :last_name, :name, :list, :metadata, to: :@object
5
+ delegate :id, :first_name, :last_name, :name, :list, :metadata, :unsubscribed_at, to: :@object
6
+
7
+ def subscribed?
8
+ @object.unsubscribed_at.nil?
9
+ end
10
+
11
+ def unsubscribed?
12
+ !subscribed?
13
+ end
6
14
  end
7
15
  end
@@ -47,6 +47,7 @@ module Nuntius
47
47
  def nuntius_list_menu
48
48
  Satis::Menus::Builder.build([:nuntius, :lists]) do |m|
49
49
  m.item :new_subscriber, link: nuntius.new_admin_list_subscriber_path(@list) if @list.persisted?
50
+ m.item :import_subscribers, link: nuntius.import_admin_list_subscribers_path(@list) if @list.persisted?
50
51
  end
51
52
  end
52
53
 
@@ -0,0 +1,9 @@
1
+ module Nuntius
2
+ class CampaignPublishJob < ApplicationJob
3
+ def perform
4
+ Nuntius::Campaign.where(state: :draft, publish_at: ..Time.current).each do |campaign|
5
+ @campaign.publish! if @campaign.can_publish?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Nuntius
6
+ class ImportSubscribersJob < ApplicationJob
7
+ KNOWN_COLUMNS = %i[first_name last_name email phone_number].freeze
8
+
9
+ def perform(list, blob, user)
10
+ csv_content = blob.download
11
+
12
+ imported = 0
13
+ failed = 0
14
+
15
+ CSV.parse(csv_content, headers: true, header_converters: :symbol) do |row|
16
+ row_hash = row.to_h
17
+ attrs = row_hash.slice(*KNOWN_COLUMNS)
18
+ extra = row_hash.except(*KNOWN_COLUMNS).reject { |_, v| v.nil? }
19
+ attrs[:metadata] = extra unless extra.empty?
20
+
21
+ subscriber = list.subscribers.new(attrs)
22
+ if subscriber.save
23
+ imported += 1
24
+ else
25
+ failed += 1
26
+ end
27
+ end
28
+
29
+ Signum.success(user, text: I18n.t("nuntius.admin.lists.subscribers.import.success", imported: imported, failed: failed))
30
+ rescue CSV::MalformedCSVError => e
31
+ Signum.error(user, text: I18n.t("nuntius.admin.lists.subscribers.import.invalid_csv", message: e.message))
32
+ ensure
33
+ blob.purge
34
+ end
35
+ end
36
+ end
@@ -3,7 +3,7 @@ module Nuntius
3
3
  def perform(account_id, months)
4
4
  messages = Nuntius::Message.distinct.select(:id).where("metadata ->> 'account_id' = :account", account: account_id)
5
5
  .where(created_at: ..months.months.ago.beginning_of_day)
6
- .where.not(status: %w[complaint bounced])
6
+ .where.not(state: %w[complaint bounced])
7
7
 
8
8
  Nuntius::Message.where(parent_message_id: messages.pluck(:id)).in_batches.update_all(parent_message_id: nil)
9
9
 
@@ -11,7 +11,7 @@ module Nuntius
11
11
  original_message = message
12
12
  message = message.dup
13
13
  message.parent_message = original_message
14
- message.status = "pending"
14
+ message.pending
15
15
  message.provider_id = ""
16
16
  end
17
17
  message.provider = provider_name
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class MessageMessenger < ApplicationMessenger
5
+ template_scope ->(screening) { all }
6
+
7
+ # For an after scope the time_range the interval is taken from the current time, the end of the
8
+ # range is 1 hour from its start.
9
+ timebased_scope :after_opened do |time_range, metadata|
10
+ Screening.where("opened_at BETWEEN ? AND ?", time_range.first, time_range.last)
11
+ end
12
+
13
+ timebased_scope :after_clicked do |time_range, metadata|
14
+ Screening.where("clicked_at BETWEEN ? AND ?", time_range.first, time_range.last)
15
+ end
16
+
17
+ timebased_scope :after_sent do |time_range, metadata|
18
+ Screening.where("last_sent_at BETWEEN ? AND ?", time_range.first, time_range.last)
19
+ end
20
+ end
21
+ end
@@ -29,36 +29,48 @@ module Nuntius
29
29
 
30
30
  before_destroy :cleanup_attachments
31
31
 
32
- # Weird loading sequence error, is fixed by the lib/nuntius/helpers
33
- # begin
34
- # has_many_attached :attachments
35
- # rescue NoMethodError
36
- # end
37
-
38
- def pending?
39
- status == "pending"
40
- end
41
-
42
- def sent?
43
- status == "sent"
44
- end
45
-
46
- def blocked?
47
- status == "blocked"
32
+ state_machine initial: :pending do
33
+ event :pending do
34
+ transition any => :pending
35
+ end
36
+ event :sent do
37
+ transition any => :sent
38
+ end
39
+ event :blocked do
40
+ transition any => :blocked
41
+ end
42
+ event :delivered do
43
+ transition any => :delivered
44
+ end
45
+ event :undelivered do
46
+ transition any => :undelivered
47
+ end
48
+ event :rejected do
49
+ transition any => :rejected
50
+ end
51
+ event :bounced do
52
+ transition any => :bounced
53
+ end
54
+ event :complaint do
55
+ transition any => :complaint
56
+ end
57
+ event :no_recipient do
58
+ transition any => :no_recipient
59
+ end
60
+ event :opened do
61
+ transition any => any
62
+ end
63
+ event :clicked do
64
+ transition any => any
65
+ end
48
66
  end
49
67
 
50
- def delivered?
51
- status == "delivered"
52
- end
68
+ nuntiable use_state_machine: true
53
69
 
54
70
  def delivered_or_blocked?
55
71
  delivered? || blocked?
56
72
  end
57
73
 
58
- def undelivered?
59
- status == "undelivered"
60
- end
61
-
62
74
  def opened?
63
75
  opened_at.present?
64
76
  end
@@ -69,7 +81,7 @@ module Nuntius
69
81
 
70
82
  # Removes only pending child messages
71
83
  def cleanup!
72
- Nuntius::Message.where(status: "pending").where(parent_message: self).destroy_all
84
+ Nuntius::Message.where(state: "pending").where(parent_message: self).destroy_all
73
85
  end
74
86
 
75
87
  def add_attachment(options)
@@ -33,10 +33,10 @@ module Nuntius
33
33
 
34
34
  response = connection.push(notification)
35
35
 
36
- message.status = if response.ok?
37
- "sent"
36
+ if response.ok?
37
+ message.sent
38
38
  else
39
- "undelivered"
39
+ message.undelivered
40
40
  end
41
41
  message
42
42
  end
@@ -14,10 +14,10 @@ module Nuntius
14
14
  options = (message.payload || {}).merge(data: {body: message.text})
15
15
  response = fcm.send([message.to], options)
16
16
 
17
- message.status = if response[:status_code] != 200 || response[:response] != "success"
18
- "undelivered"
17
+ if response[:status_code] != 200 || response[:response] != "success"
18
+ message.undelivered
19
19
  else
20
- "sent"
20
+ message.sent
21
21
  end
22
22
 
23
23
  message
@@ -16,15 +16,15 @@ module Nuntius
16
16
  def deliver
17
17
  response = client.message_create(message.from.present? ? message.from : from, message.to, message.text)
18
18
  message.provider_id = response.id
19
- message.status = translated_status(response.recipients["items"].first.status)
19
+ message.send(translated_status(response.recipients["items"].first.status))
20
20
  message
21
21
  end
22
22
 
23
23
  def refresh
24
24
  response = client.message(message.provider_id)
25
25
  message.provider_id = response.id
26
- message.status = translated_status(response.recipients["items"].first.status)
27
- Nuntius.config.logger.call.info "SMS #{message.to} status: #{message.status}"
26
+ message.send(translated_status(response.recipients["items"].first.status))
27
+ Nuntius.config.logger.call.info "SMS #{message.to} state: #{message.state}"
28
28
  message
29
29
  rescue => _e
30
30
  message
@@ -24,10 +24,10 @@ module Nuntius
24
24
  args = (message.payload || {}).merge(channel: message[:to], text: message.text, as_user: true, username: message[:from])
25
25
  response = client.chat_postMessage(args.deep_symbolize_keys)
26
26
 
27
- message.status = if response["ok"]
28
- "sent"
27
+ if response["ok"]
28
+ message.sent
29
29
  else
30
- "undelivered"
30
+ message.undelivered
31
31
  end
32
32
 
33
33
  message
@@ -17,7 +17,7 @@ module Nuntius
17
17
  def deliver
18
18
  response = client.messages.create(sender: message.from.present? ? message.from : from, to: message.to, message: message.text)
19
19
  message.provider_id = response.body.messageid
20
- message.status = translated_status(response.status)
20
+ message.send(translated_status(response.status))
21
21
  message
22
22
  end
23
23
 
@@ -17,7 +17,7 @@ module Nuntius
17
17
  def deliver
18
18
  return no_recipient if message.to.blank?
19
19
  return block unless MailAllowList.new(settings[:allow_list]).allowed?(message.to)
20
- return block if Nuntius::Message.where(status: %w[complaint bounced], to: message.to).count >= 1
20
+ return block if Nuntius::Message.where(state: %w[complaint bounced], to: message.to).count >= 1
21
21
 
22
22
  mail = if message.from.present?
23
23
  Mail.new(sender: from_header, from: message.from)
@@ -73,28 +73,28 @@ module Nuntius
73
73
  begin
74
74
  response = mail.deliver!
75
75
  rescue Net::SMTPFatalError
76
- message.status = "rejected"
76
+ message.rejected
77
77
  return message
78
78
  rescue Net::SMTPServerBusy, Net::ReadTimeout
79
- message.status = "undelivered"
79
+ message.undelivered
80
80
  return message
81
81
  end
82
82
 
83
83
  message.provider_id = mail.message_id
84
- message.status = "undelivered"
85
- message.status = "sent" if Rails.env.test? || response.success?
84
+ message.undelivered
85
+ message.sent if Rails.env.test? || response.success?
86
86
  message.last_sent_at = Time.zone.now if message.sent?
87
87
 
88
88
  message
89
89
  end
90
90
 
91
91
  def block
92
- message.status = "blocked"
92
+ message.blocked
93
93
  message
94
94
  end
95
95
 
96
96
  def no_recipient
97
- message.status = "no_recipient"
97
+ message.no_recipient
98
98
  message
99
99
  end
100
100
 
@@ -13,10 +13,10 @@ module Nuntius
13
13
  args = (message.payload || {}).merge(text: message.text)
14
14
  response = Faraday.post(message[:to], JSON.dump(args), {"Content-Type": "application/json"})
15
15
 
16
- message.status = if response.status == 200
17
- "sent"
16
+ if response.status == 200
17
+ message.sent
18
18
  else
19
- "undelivered"
19
+ message.undelivered
20
20
  end
21
21
 
22
22
  message
@@ -17,14 +17,14 @@ module Nuntius
17
17
  def deliver
18
18
  response = client.messages.create(from: message.from.present? ? message.from : from, to: message.to, body: message.text)
19
19
  message.provider_id = response.sid
20
- message.status = translated_status(response.status)
20
+ message.send(translated_status(response.status))
21
21
  message
22
22
  end
23
23
 
24
24
  def refresh
25
25
  response = client.messages(message.provider_id).fetch
26
26
  message.provider_id = response.sid
27
- message.status = translated_status(response.status)
27
+ message.send(translated_status(response.status))
28
28
  message
29
29
  end
30
30
 
@@ -19,14 +19,14 @@ module Nuntius
19
19
  # Need hostname here too
20
20
  response = client.calls.create(from: message.from.present? ? message.from : from, to: message.to, method: "POST", url: callback_url)
21
21
  message.provider_id = response.sid
22
- message.status = translated_status(response.status)
22
+ message.send(translated_status(response.status))
23
23
  message
24
24
  end
25
25
 
26
26
  def refresh
27
27
  response = client.calls(message.provider_id).fetch
28
28
  message.provider_id = response.sid
29
- message.status = translated_status(response.status)
29
+ message.send(translated_status(response.status))
30
30
  message
31
31
  end
32
32
 
@@ -53,19 +53,19 @@ module Nuntius
53
53
  private
54
54
 
55
55
  def process_delivery
56
- message.status = :delivered
56
+ message.delivered
57
57
  message.metadata[:feedback] = {type: "delivery", info: context.notification["delivery"]}
58
58
  message.save!
59
59
  end
60
60
 
61
61
  def process_bounce
62
- message.status = :bounced
62
+ message.bounced
63
63
  message.metadata[:feedback] = {type: "bounce", info: context.notification["bounce"]}
64
64
  message.save!
65
65
  end
66
66
 
67
67
  def process_complaint
68
- message.status = :complaint
68
+ message.complaint
69
69
  message.metadata[:feedback] = {type: "complaint", info: context.notification["complaint"]}
70
70
  message.save!
71
71
  end
@@ -50,7 +50,7 @@ module Nuntius
50
50
  end
51
51
  end
52
52
  end
53
- column(:status)
53
+ column(:state)
54
54
 
55
55
  order created_at: :desc
56
56
 
@@ -24,10 +24,12 @@
24
24
  .grid.grid-cols-12.gap-4 data-toggle-target="insertion"
25
25
 
26
26
  template data-toggle-target='toggleable' data-toggle-value='mail'
27
- .col-span-6
27
+ .col-span-4
28
28
  = f.input :link_tracking, as: :switch
29
- .col-span-6
29
+ .col-span-4
30
30
  = f.input :open_tracking, as: :switch
31
+ .col-span-4
32
+ = f.input :publish_at, as: :date_time
31
33
  .col-span-12
32
34
  = f.input :subject
33
35
  .col-span-12
@@ -51,7 +53,7 @@
51
53
  = @campaign.messages.where(status: "sent").count
52
54
  .flex.flex-col.bg-gray-200.p-8
53
55
  dt.text-sm.font-semibold.leading-6.text-gray-600
54
- = t(".messages_not_sent")
56
+ = t(".messages_not_sent")
55
57
  dd.order-first.text-3xl.font-semibold.tracking-tight.text-gray-900
56
58
  = @campaign.messages.where.not(status: "sent").count
57
59
  .flex.flex-col.bg-gray-200.p-8
@@ -64,6 +66,6 @@
64
66
  = t(".messages_clicked")
65
67
  dd.order-first.text-3xl.font-semibold.tracking-tight.text-gray-900
66
68
  = @campaign.messages.where("click_count >= 1").count
67
-
69
+
68
70
  - card.with_tab :messages, padding: false do |tab|
69
71
  = sts.table :"nuntius/campaign_messages", params: { campaign_id: @campaign.id }
@@ -0,0 +1,15 @@
1
+ = form_with url: import_admin_list_subscribers_path(@list), multipart: true do |f|
2
+ = sts.card :nuntius_admin_lists_subscribers_import, title: t('.title'), icon: 'fad fa-file-import' do |card|
3
+ - card.with_action
4
+ = f.submit t('.import'), class: 'btn btn-primary'
5
+
6
+ .grid.grid-cols-12.gap-4.p-4
7
+ .col-span-12
8
+ p.text-sm.text-gray-600= t('.hint')
9
+
10
+ .col-span-12
11
+ label.block.text-sm.font-medium.mb-1= t('.file_label')
12
+ = f.file_field :file, accept: '.csv', class: 'block w-full text-sm'
13
+
14
+ .col-span-12
15
+ p.text-xs.text-gray-500= t('.columns_hint')
@@ -26,7 +26,7 @@
26
26
 
27
27
  - card.with_tab:details, padding: true
28
28
  = sts.info class: "grid grid-cols-1 gap-4 sm:grid-cols-3" do |info|
29
- = info.with_item :status, content: @message.status, class: "sm:col-span-1"
29
+ = info.with_item :state, content: @message.state, class: "sm:col-span-1"
30
30
  = info.with_item :transport, content: @message.transport, class: "sm:col-span-1"
31
31
  = info.with_item :provider, content: @message.provider, class: "sm:col-span-1"
32
32
  = info.with_item :provider_id, content: @message.provider_id, class: "sm:col-span-1"
@@ -0,0 +1,20 @@
1
+ = sts.form_for(@subscriber, url: @subscriber.new_record? ? subscribers_path(@list) : subscriber_path(@subscriber), html: {multipart: true}) do |f|
2
+ = f.hidden_field :list_id
3
+ = sts.card :nuntius_admin_lists_subscribers, title: t('.subscribe_to_list', list: @subscriber.list.name), icon: 'fad fa-address-card' do |card|
4
+ - card.with_action
5
+ = f.button class: 'button primary' do
6
+ - if @subscriber.new_record?
7
+ = t('.create')
8
+ - else
9
+ = t('.update')
10
+
11
+
12
+ .grid.grid-cols-12.gap-4
13
+ .col-span-6
14
+ = f.input :first_name
15
+ .col-span-6
16
+ = f.input :last_name
17
+ .col-span-12
18
+ = f.input :phone_number
19
+ .col-span-12
20
+ = f.input :email
@@ -47,6 +47,7 @@ en:
47
47
  lists:
48
48
  new: New list
49
49
  new_subscriber: New subscriber
50
+ import_subscribers: Import subscribers
50
51
  locales:
51
52
  new: New locale
52
53
  templates:
@@ -113,6 +114,16 @@ en:
113
114
  edit:
114
115
  metadata: Metadata
115
116
  subscribers: Subscribers
117
+ import:
118
+ title: Import subscribers
119
+ file_label: CSV file
120
+ import: Import
121
+ hint: "Upload a CSV file to bulk import subscribers into this list. The first row must contain column headers."
122
+ columns_hint: "Supported columns: first_name, last_name, email, phone_number. Any additional columns will be stored in the subscriber's metadata."
123
+ queued: "Import started, you will be notified when it completes."
124
+ no_file: Please select a CSV file to import.
125
+ invalid_csv: "Invalid CSV file: %{message}"
126
+ success: "Import complete: %{imported} subscriber(s) imported, %{failed} failed."
116
127
  locales:
117
128
  edit:
118
129
  card:
@@ -180,6 +191,10 @@ en:
180
191
  Nuntius templates: Templates
181
192
  title: Templates
182
193
  subscribers:
194
+ edit:
195
+ subscribe_to_list: Subscribe to %{list}
196
+ create: Subscribe
197
+ update: Update
183
198
  show:
184
199
  unsubscribe: Unsubscribe
185
200
  resubscribe: Resubscribe
@@ -42,6 +42,7 @@ nl:
42
42
  click_count: Aantal geklikt
43
43
  campaign_id: Campagne
44
44
  created_at: Aangemaakt om
45
+ state: Status
45
46
  models:
46
47
  campaign: Campagne
47
48
  layout: Layout
@@ -59,6 +60,7 @@ nl:
59
60
  lists:
60
61
  new: Nieuwe lijst
61
62
  new_subscriber: Nieuwe abonnee
63
+ import_subscribers: Importeer abonnees
62
64
  locales:
63
65
  new: Nieuwe locale
64
66
  templates:
@@ -126,6 +128,16 @@ nl:
126
128
  edit:
127
129
  metadata: Metadata
128
130
  subscribers: Abonnees
131
+ import:
132
+ title: Abonnees importeren
133
+ file_label: CSV-bestand
134
+ import: Importeer
135
+ no_file: Geen bestand geselecteerd
136
+ invalid_csv: "Ongeldig CSV-bestand: %{message}"
137
+ queued: "Import gestart, je ontvangt een melding zodra de import klaar is."
138
+ success: "%{imported} abonnee(s) geïmporteerd, %{failed} mislukt"
139
+ hint: Upload een CSV-bestand om abonnees in bulk te importeren. De eerste rij moet de kolomkoppen bevatten.
140
+ columns_hint: "Ondersteunde kolommen: first_name, last_name, email, phone_number. Extra kolommen worden opgeslagen in metadata."
129
141
  locales:
130
142
  edit:
131
143
  card:
@@ -163,7 +175,7 @@ nl:
163
175
  provider_id: Provider ID
164
176
  refreshes: Refreshes
165
177
  request_id: Request ID
166
- status: Status
178
+ state: Status
167
179
  subject: Onderwerp
168
180
  to: Aan
169
181
  transport: Transport
@@ -195,6 +207,10 @@ nl:
195
207
  Nuntius templates: Templates
196
208
  title: Templates
197
209
  subscribers:
210
+ edit:
211
+ subscribe_to_list: Abbonnement op %{list}
212
+ create: Abbonneren
213
+ update: Aanpassen
198
214
  show:
199
215
  unsubscribe: Uitschrijven
200
216
  resubscribe: Opnieuw inschrijven
data/config/routes.rb CHANGED
@@ -41,7 +41,12 @@ Nuntius::Engine.routes.draw do
41
41
  resources :attachments, controller: "satis/attachments"
42
42
  end
43
43
  resources :lists do
44
- resources :subscribers, controller: "lists/subscribers"
44
+ resources :subscribers, controller: "lists/subscribers" do
45
+ collection do
46
+ get :import
47
+ post :import
48
+ end
49
+ end
45
50
  end
46
51
  resources :messages do
47
52
  member do
@@ -0,0 +1,5 @@
1
+ class AddPublishAtToCampaign < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :nuntius_campaigns, :publish_at, :datetime
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class RenameStatusToState < ActiveRecord::Migration[8.1]
2
+ def change
3
+ rename_column :nuntius_messages, :status, :state
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nuntius
4
- VERSION = "1.4.15"
4
+ VERSION = "1.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nuntius
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.15
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-11 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: apnotic
@@ -442,7 +442,9 @@ files:
442
442
  - app/exceptions/nuntius/missing_messenger_exception.rb
443
443
  - app/helpers/nuntius/application_helper.rb
444
444
  - app/jobs/nuntius/application_job.rb
445
+ - app/jobs/nuntius/campaign_publish_job.rb
445
446
  - app/jobs/nuntius/deliver_inbound_message_job.rb
447
+ - app/jobs/nuntius/import_subscribers_job.rb
446
448
  - app/jobs/nuntius/messenger_job.rb
447
449
  - app/jobs/nuntius/purge_message_job.rb
448
450
  - app/jobs/nuntius/retrieve_mail_job.rb
@@ -452,6 +454,7 @@ files:
452
454
  - app/message_boxes/nuntius/base_message_box.rb
453
455
  - app/messengers/nuntius/base_messenger.rb
454
456
  - app/messengers/nuntius/custom_messenger.rb
457
+ - app/messengers/nuntius/message_messenger.rb
455
458
  - app/models/nuntius/application_record.rb
456
459
  - app/models/nuntius/attachment.rb
457
460
  - app/models/nuntius/campaign.rb
@@ -511,6 +514,7 @@ files:
511
514
  - app/views/nuntius/admin/lists/edit.html.slim
512
515
  - app/views/nuntius/admin/lists/index.html.slim
513
516
  - app/views/nuntius/admin/lists/subscribers/edit.html.slim
517
+ - app/views/nuntius/admin/lists/subscribers/import.html.slim
514
518
  - app/views/nuntius/admin/locales/edit.html.slim
515
519
  - app/views/nuntius/admin/locales/index.html.slim
516
520
  - app/views/nuntius/admin/messages/index.html.slim
@@ -519,6 +523,7 @@ files:
519
523
  - app/views/nuntius/admin/templates/index.html.slim
520
524
  - app/views/nuntius/dashboard/show.html.slim
521
525
  - app/views/nuntius/messages/show.html.slim
526
+ - app/views/nuntius/subscribers/edit.html.slim
522
527
  - app/views/nuntius/subscribers/show.html.slim
523
528
  - config/locales/en.yml
524
529
  - config/locales/nl.yml
@@ -554,6 +559,8 @@ files:
554
559
  - db/migrate/20260210122500_add_tracking_to_nuntius_messages.rb
555
560
  - db/migrate/20260225123822_add_metadata_to_subscriber.rb
556
561
  - db/migrate/20260310153208_add_subscriber_to_message.rb
562
+ - db/migrate/20260324140324_add_publish_at_to_campaign.rb
563
+ - db/migrate/20260324150714_rename_status_to_state.rb
557
564
  - lib/generators/nuntius/install_generator.rb
558
565
  - lib/generators/nuntius/tailwind_config_generator.rb
559
566
  - lib/generators/nuntius/templates/config/initializers/nuntius.rb