From 46302c4e82cbbfae0308a021e23079fc1f675850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Fri, 19 Dec 2025 21:21:51 +0100 Subject: [PATCH 1/6] add mail interceptor --- README.md | 1 + .../decidim/pokecode/mail_interceptor.rb | 51 ++++++ lib/decidim/pokecode/configuration.rb | 8 + lib/decidim/pokecode/engine.rb | 9 + spec/lib/mail_interceptor_spec.rb | 168 ++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 app/mailers/decidim/pokecode/mail_interceptor.rb create mode 100644 spec/lib/mail_interceptor_spec.rb diff --git a/README.md b/README.md index a186f1d..aed6415 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This plugin relies on the command `decidim:upgrade` to make sure common files ar | `AWS_PUBLIC` | Usually, to be used in combination with the previous option. This generates assets without signatures, which basically means they don't expire. | `true` | | | `AWS_FORCE_PATH_STYLE` | Certain providers do not support the bucket name as the subdomain of the AWS endpoint (ie: Contabo). Set to `true` if that's the case. | `false` | | | `CONTENT_SECURITY_POLICY` | Sets custom Content Security Policy headers for enhanced security. When set, it is added to the default CSP configuration. | `""` (disabled) | | +| `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send and email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. All values are checked as suffixes of the emails being sent. Leave empty to disable any interception. | `""` | ## Installation diff --git a/app/mailers/decidim/pokecode/mail_interceptor.rb b/app/mailers/decidim/pokecode/mail_interceptor.rb new file mode 100644 index 0000000..427138d --- /dev/null +++ b/app/mailers/decidim/pokecode/mail_interceptor.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Decidim + module Pokecode + # Email interceptor that filters outgoing emails based on allowed recipients. + # Only allows emails to recipients that match the configured allowed list. + class MailInterceptor + def self.delivering_email(message) + return if allowed_recipients_empty? + + # Get all recipients from the email message + to_recipients = Array(message.to).map(&:downcase) + cc_recipients = Array(message.cc).map(&:downcase) + bcc_recipients = Array(message.bcc).map(&:downcase) + + all_recipients = to_recipients + cc_recipients + bcc_recipients + + # Filter recipients to only those in the allowed list + filtered_recipients = all_recipients.select { |recipient| allowed?(recipient) } + + if filtered_recipients.empty? + Rails.logger.warn "[Decidim::Pokecode] Email delivery intercepted. No recipients matched allowed list. Original recipients: #{all_recipients.join(", ")}" + message.perform_deliveries = false + else + # Update the message with only the allowed recipients + message.to = (to_recipients & filtered_recipients).uniq + message.cc = (cc_recipients & filtered_recipients).uniq + message.bcc = (bcc_recipients & filtered_recipients).uniq + + Rails.logger.info "[Decidim::Pokecode] Email delivery filtered. Original recipients: #{all_recipients.join(", ")}, Allowed recipients: #{filtered_recipients.join(", ")}" + end + end + + private + + def self.allowed_recipients_empty? + Decidim::Pokecode.allowed_recipients_list.empty? + end + + def self.allowed?(email) + email_lowercase = email.downcase + Decidim::Pokecode.allowed_recipients_list.any? do |allowed| + allowed_lowercase = allowed.downcase + # Check if the email matches or ends with the allowed value + # This allows both exact email matches and domain-based matches (e.g., @pokecode.net) + email_lowercase == allowed_lowercase || email_lowercase.end_with?("#{allowed_lowercase}".start_with?("@") ? allowed_lowercase : "@#{allowed_lowercase}") + end + end + end + end +end diff --git a/lib/decidim/pokecode/configuration.rb b/lib/decidim/pokecode/configuration.rb index 7e7e4c4..a120730 100644 --- a/lib/decidim/pokecode/configuration.rb +++ b/lib/decidim/pokecode/configuration.rb @@ -65,6 +65,10 @@ module Pokecode host.present? && host.starts_with?("https://") ? host : "" end + config_accessor :allowed_recipients do + Decidim::Env.new("ALLOWED_RECIPIENTS", "").value + end + config_accessor :content_security_policies_extra do { "connect-src" => ENV.fetch("CONTENT_SECURITY_POLICY", "").split, @@ -86,6 +90,10 @@ def self.rack_attack_ips Pokecode.rack_attack_allowed_ips&.split(/[,\s]+/)&.reject(&:blank?) || [] end + def self.allowed_recipients_list + Pokecode.allowed_recipients&.split(/[,\s]+/)&.reject(&:blank?) || [] + end + def self.deface_enabled Pokecode.pokecode_footer_enabled || Decidim::Pokecode.language_menu_enabled end diff --git a/lib/decidim/pokecode/engine.rb b/lib/decidim/pokecode/engine.rb index ed4cc65..14799ef 100644 --- a/lib/decidim/pokecode/engine.rb +++ b/lib/decidim/pokecode/engine.rb @@ -168,6 +168,15 @@ class Engine < ::Rails::Engine initializer "pokecode.shakapacker.assets_path" do Decidim.register_assets_path File.expand_path("app/packs", root) end + + initializer "pokecode.mail_interceptor" do + if Decidim::Pokecode.allowed_recipients_list.present? + Mail.register_interceptor(Decidim::Pokecode::MailInterceptor) + Rails.logger.info "[Decidim::Pokecode] Email interceptor enabled. Allowed recipients: #{Decidim::Pokecode.allowed_recipients_list.join(", ")}" + else + Rails.logger.info "[Decidim::Pokecode] Email interceptor disabled." + end + end end end end diff --git a/spec/lib/mail_interceptor_spec.rb b/spec/lib/mail_interceptor_spec.rb new file mode 100644 index 0000000..20b4f30 --- /dev/null +++ b/spec/lib/mail_interceptor_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Pokecode::MailInterceptor do + let(:message) do + instance_double( + Mail::Message, + to: to_recipients, + cc: cc_recipients, + bcc: bcc_recipients, + perform_deliveries: true + ) + end + + let(:to_recipients) { [] } + let(:cc_recipients) { [] } + let(:bcc_recipients) { [] } + let(:allowed_recipients) { [] } + + before do + allow(Rails.logger).to receive(:warn) + allow(Rails.logger).to receive(:info) + allow(message).to receive(:to=) + allow(message).to receive(:cc=) + allow(message).to receive(:bcc=) + allow(message).to receive(:perform_deliveries=) + end + + describe ".delivering_email" do + before do + allow(Decidim::Pokecode).to receive(:allowed_recipients_list).and_return(allowed_recipients) + end + + context "when allowed_recipients is empty" do + it "does not intercept the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + end + end + + context "when allowed_recipients is configured" do + context "with domain-based allowed recipients" do + let(:allowed_recipients) { ["@pokecode.net"] } + + context "when recipients match the allowed domain" do + let(:to_recipients) { ["user@pokecode.net", "admin@pokecode.net"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["user@pokecode.net", "admin@pokecode.net"].uniq) + end + + it "logs the filtering" do + described_class.delivering_email(message) + expect(Rails.logger).to have_received(:info) + end + end + + context "when recipients don't match the allowed domain" do + let(:to_recipients) { ["user@example.com", "admin@gmail.com"] } + + it "blocks the email" do + described_class.delivering_email(message) + expect(message).to have_received(:perform_deliveries=).with(false) + end + + it "logs the interception" do + described_class.delivering_email(message) + expect(Rails.logger).to have_received(:warn) + end + end + + context "with mixed recipients" do + let(:to_recipients) { ["user@pokecode.net", "admin@example.com"] } + let(:cc_recipients) { ["cc@pokecode.net"] } + let(:bcc_recipients) { ["bcc@gmail.com"] } + + it "filters to only allowed recipients" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net"]) + expect(message).to have_received(:cc=).with(["cc@pokecode.net"]) + expect(message).to have_received(:bcc=).with([]) + end + end + end + + context "with exact email allowed recipients" do + let(:allowed_recipients) { ["john@example.com", "jane@example.com"] } + + context "when recipients match exactly" do + let(:to_recipients) { ["john@example.com"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["john@example.com"]) + end + end + + context "when recipients don't match" do + let(:to_recipients) { ["bob@example.com"] } + + it "blocks the email" do + described_class.delivering_email(message) + expect(message).to have_received(:perform_deliveries=).with(false) + end + end + end + + context "with mixed allowed recipients" do + let(:allowed_recipients) { ["@pokecode.net", "external@gmail.com"] } + + context "when recipients match domain or exact email" do + let(:to_recipients) { ["user@pokecode.net", "external@gmail.com"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["user@pokecode.net", "external@gmail.com"]) + end + end + + context "when some recipients don't match" do + let(:to_recipients) { ["user@pokecode.net", "other@gmail.com"] } + + it "filters to only allowed recipients" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net"]) + end + end + end + + context "with case-insensitive matching" do + let(:allowed_recipients) { ["@pokecode.net", "John@Example.Com"] } + + context "with uppercase recipients" do + let(:to_recipients) { ["USER@POKECODE.NET", "JOHN@EXAMPLE.COM"] } + + it "matches case-insensitively" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) + end + end + + context "with mixed case recipients" do + let(:to_recipients) { ["user@PokeCode.Net", "John@example.com"] } + + it "matches case-insensitively" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) + end + end + end + + context "when only BCC recipients match" do + let(:allowed_recipients) { ["@pokecode.net"] } + let(:bcc_recipients) { ["admin@pokecode.net"] } + + it "allows the email with only BCC" do + described_class.delivering_email(message) + expect(message).to have_received(:bcc=).with(["admin@pokecode.net"]) + end + end + end + end +end From 67ea8610a12f69dca66af6afef3eed658b0cf19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Fri, 19 Dec 2025 22:04:02 +0100 Subject: [PATCH 2/6] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aed6415..ab3d6cd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This plugin relies on the command `decidim:upgrade` to make sure common files ar | `AWS_PUBLIC` | Usually, to be used in combination with the previous option. This generates assets without signatures, which basically means they don't expire. | `true` | | | `AWS_FORCE_PATH_STYLE` | Certain providers do not support the bucket name as the subdomain of the AWS endpoint (ie: Contabo). Set to `true` if that's the case. | `false` | | | `CONTENT_SECURITY_POLICY` | Sets custom Content Security Policy headers for enhanced security. When set, it is added to the default CSP configuration. | `""` (disabled) | | -| `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send and email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. All values are checked as suffixes of the emails being sent. Leave empty to disable any interception. | `""` | +| `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send and email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. Exact email addresses (without a leading `@`) must match the full recipient email, while domain patterns starting with `@` are matched as suffixes of the recipient email. Leave empty to disable any interception. | `""` | ## Installation From 90885d79b6e59689763481d2ad447670dc78d3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Fri, 19 Dec 2025 22:05:50 +0100 Subject: [PATCH 3/6] rubocop --- .../decidim/pokecode/mail_interceptor.rb | 4 +- spec/lib/mail_interceptor_spec.rb | 168 ----------------- spec/mailers/mail_interceptor_spec.rb | 170 ++++++++++++++++++ 3 files changed, 171 insertions(+), 171 deletions(-) delete mode 100644 spec/lib/mail_interceptor_spec.rb create mode 100644 spec/mailers/mail_interceptor_spec.rb diff --git a/app/mailers/decidim/pokecode/mail_interceptor.rb b/app/mailers/decidim/pokecode/mail_interceptor.rb index 427138d..0538a06 100644 --- a/app/mailers/decidim/pokecode/mail_interceptor.rb +++ b/app/mailers/decidim/pokecode/mail_interceptor.rb @@ -31,8 +31,6 @@ def self.delivering_email(message) end end - private - def self.allowed_recipients_empty? Decidim::Pokecode.allowed_recipients_list.empty? end @@ -43,7 +41,7 @@ def self.allowed?(email) allowed_lowercase = allowed.downcase # Check if the email matches or ends with the allowed value # This allows both exact email matches and domain-based matches (e.g., @pokecode.net) - email_lowercase == allowed_lowercase || email_lowercase.end_with?("#{allowed_lowercase}".start_with?("@") ? allowed_lowercase : "@#{allowed_lowercase}") + email_lowercase == allowed_lowercase || email_lowercase.end_with?(allowed_lowercase.to_s.start_with?("@") ? allowed_lowercase : "@#{allowed_lowercase}") end end end diff --git a/spec/lib/mail_interceptor_spec.rb b/spec/lib/mail_interceptor_spec.rb deleted file mode 100644 index 20b4f30..0000000 --- a/spec/lib/mail_interceptor_spec.rb +++ /dev/null @@ -1,168 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Pokecode::MailInterceptor do - let(:message) do - instance_double( - Mail::Message, - to: to_recipients, - cc: cc_recipients, - bcc: bcc_recipients, - perform_deliveries: true - ) - end - - let(:to_recipients) { [] } - let(:cc_recipients) { [] } - let(:bcc_recipients) { [] } - let(:allowed_recipients) { [] } - - before do - allow(Rails.logger).to receive(:warn) - allow(Rails.logger).to receive(:info) - allow(message).to receive(:to=) - allow(message).to receive(:cc=) - allow(message).to receive(:bcc=) - allow(message).to receive(:perform_deliveries=) - end - - describe ".delivering_email" do - before do - allow(Decidim::Pokecode).to receive(:allowed_recipients_list).and_return(allowed_recipients) - end - - context "when allowed_recipients is empty" do - it "does not intercept the email" do - described_class.delivering_email(message) - expect(message).not_to have_received(:perform_deliveries=) - end - end - - context "when allowed_recipients is configured" do - context "with domain-based allowed recipients" do - let(:allowed_recipients) { ["@pokecode.net"] } - - context "when recipients match the allowed domain" do - let(:to_recipients) { ["user@pokecode.net", "admin@pokecode.net"] } - - it "allows the email" do - described_class.delivering_email(message) - expect(message).not_to have_received(:perform_deliveries=) - expect(message).to have_received(:to=).with(["user@pokecode.net", "admin@pokecode.net"].uniq) - end - - it "logs the filtering" do - described_class.delivering_email(message) - expect(Rails.logger).to have_received(:info) - end - end - - context "when recipients don't match the allowed domain" do - let(:to_recipients) { ["user@example.com", "admin@gmail.com"] } - - it "blocks the email" do - described_class.delivering_email(message) - expect(message).to have_received(:perform_deliveries=).with(false) - end - - it "logs the interception" do - described_class.delivering_email(message) - expect(Rails.logger).to have_received(:warn) - end - end - - context "with mixed recipients" do - let(:to_recipients) { ["user@pokecode.net", "admin@example.com"] } - let(:cc_recipients) { ["cc@pokecode.net"] } - let(:bcc_recipients) { ["bcc@gmail.com"] } - - it "filters to only allowed recipients" do - described_class.delivering_email(message) - expect(message).to have_received(:to=).with(["user@pokecode.net"]) - expect(message).to have_received(:cc=).with(["cc@pokecode.net"]) - expect(message).to have_received(:bcc=).with([]) - end - end - end - - context "with exact email allowed recipients" do - let(:allowed_recipients) { ["john@example.com", "jane@example.com"] } - - context "when recipients match exactly" do - let(:to_recipients) { ["john@example.com"] } - - it "allows the email" do - described_class.delivering_email(message) - expect(message).not_to have_received(:perform_deliveries=) - expect(message).to have_received(:to=).with(["john@example.com"]) - end - end - - context "when recipients don't match" do - let(:to_recipients) { ["bob@example.com"] } - - it "blocks the email" do - described_class.delivering_email(message) - expect(message).to have_received(:perform_deliveries=).with(false) - end - end - end - - context "with mixed allowed recipients" do - let(:allowed_recipients) { ["@pokecode.net", "external@gmail.com"] } - - context "when recipients match domain or exact email" do - let(:to_recipients) { ["user@pokecode.net", "external@gmail.com"] } - - it "allows the email" do - described_class.delivering_email(message) - expect(message).not_to have_received(:perform_deliveries=) - expect(message).to have_received(:to=).with(["user@pokecode.net", "external@gmail.com"]) - end - end - - context "when some recipients don't match" do - let(:to_recipients) { ["user@pokecode.net", "other@gmail.com"] } - - it "filters to only allowed recipients" do - described_class.delivering_email(message) - expect(message).to have_received(:to=).with(["user@pokecode.net"]) - end - end - end - - context "with case-insensitive matching" do - let(:allowed_recipients) { ["@pokecode.net", "John@Example.Com"] } - - context "with uppercase recipients" do - let(:to_recipients) { ["USER@POKECODE.NET", "JOHN@EXAMPLE.COM"] } - - it "matches case-insensitively" do - described_class.delivering_email(message) - expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) - end - end - - context "with mixed case recipients" do - let(:to_recipients) { ["user@PokeCode.Net", "John@example.com"] } - - it "matches case-insensitively" do - described_class.delivering_email(message) - expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) - end - end - end - - context "when only BCC recipients match" do - let(:allowed_recipients) { ["@pokecode.net"] } - let(:bcc_recipients) { ["admin@pokecode.net"] } - - it "allows the email with only BCC" do - described_class.delivering_email(message) - expect(message).to have_received(:bcc=).with(["admin@pokecode.net"]) - end - end - end - end -end diff --git a/spec/mailers/mail_interceptor_spec.rb b/spec/mailers/mail_interceptor_spec.rb new file mode 100644 index 0000000..ce852d0 --- /dev/null +++ b/spec/mailers/mail_interceptor_spec.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Pokecode + describe MailInterceptor do + let(:message) do + instance_double( + Mail::Message, + to: to_recipients, + cc: cc_recipients, + bcc: bcc_recipients, + perform_deliveries: true + ) + end + + let(:to_recipients) { [] } + let(:cc_recipients) { [] } + let(:bcc_recipients) { [] } + let(:allowed_recipients) { [] } + + before do + allow(Rails.logger).to receive(:warn) + allow(Rails.logger).to receive(:info) + allow(message).to receive(:to=) + allow(message).to receive(:cc=) + allow(message).to receive(:bcc=) + allow(message).to receive(:perform_deliveries=) + end + + describe ".delivering_email" do + before do + allow(Decidim::Pokecode).to receive(:allowed_recipients_list).and_return(allowed_recipients) + end + + context "when allowed_recipients is empty" do + it "does not intercept the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + end + end + + context "when allowed_recipients is configured" do + context "with domain-based allowed recipients" do + let(:allowed_recipients) { ["@pokecode.net"] } + + context "when recipients match the allowed domain" do + let(:to_recipients) { ["user@pokecode.net", "admin@pokecode.net"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["user@pokecode.net", "admin@pokecode.net"].uniq) + end + + it "logs the filtering" do + described_class.delivering_email(message) + expect(Rails.logger).to have_received(:info) + end + end + + context "when recipients don't match the allowed domain" do + let(:to_recipients) { ["user@example.com", "admin@gmail.com"] } + + it "blocks the email" do + described_class.delivering_email(message) + expect(message).to have_received(:perform_deliveries=).with(false) + end + + it "logs the interception" do + described_class.delivering_email(message) + expect(Rails.logger).to have_received(:warn) + end + end + + context "with mixed recipients" do + let(:to_recipients) { ["user@pokecode.net", "admin@example.com"] } + let(:cc_recipients) { ["cc@pokecode.net"] } + let(:bcc_recipients) { ["bcc@gmail.com"] } + + it "filters to only allowed recipients" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net"]) + expect(message).to have_received(:cc=).with(["cc@pokecode.net"]) + expect(message).to have_received(:bcc=).with([]) + end + end + end + + context "with exact email allowed recipients" do + let(:allowed_recipients) { ["john@example.com", "jane@example.com"] } + + context "when recipients match exactly" do + let(:to_recipients) { ["john@example.com"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["john@example.com"]) + end + end + + context "when recipients don't match" do + let(:to_recipients) { ["bob@example.com"] } + + it "blocks the email" do + described_class.delivering_email(message) + expect(message).to have_received(:perform_deliveries=).with(false) + end + end + end + + context "with mixed allowed recipients" do + let(:allowed_recipients) { ["@pokecode.net", "external@gmail.com"] } + + context "when recipients match domain or exact email" do + let(:to_recipients) { ["user@pokecode.net", "external@gmail.com"] } + + it "allows the email" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + expect(message).to have_received(:to=).with(["user@pokecode.net", "external@gmail.com"]) + end + end + + context "when some recipients don't match" do + let(:to_recipients) { ["user@pokecode.net", "other@gmail.com"] } + + it "filters to only allowed recipients" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net"]) + end + end + end + + context "with case-insensitive matching" do + let(:allowed_recipients) { ["@pokecode.net", "John@Example.Com"] } + + context "with uppercase recipients" do + let(:to_recipients) { ["USER@POKECODE.NET", "JOHN@EXAMPLE.COM"] } + + it "matches case-insensitively" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) + end + end + + context "with mixed case recipients" do + let(:to_recipients) { ["user@PokeCode.Net", "John@example.com"] } + + it "matches case-insensitively" do + described_class.delivering_email(message) + expect(message).to have_received(:to=).with(["user@pokecode.net", "john@example.com"]) + end + end + end + + context "when only BCC recipients match" do + let(:allowed_recipients) { ["@pokecode.net"] } + let(:bcc_recipients) { ["admin@pokecode.net"] } + + it "allows the email with only BCC" do + described_class.delivering_email(message) + expect(message).to have_received(:bcc=).with(["admin@pokecode.net"]) + end + end + end + end + end +end From d53babb66e4f288ef0cc38016e5258887ff63917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Fri, 19 Dec 2025 22:08:42 +0100 Subject: [PATCH 4/6] spell --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab3d6cd..df4c2b2 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This plugin relies on the command `decidim:upgrade` to make sure common files ar | `AWS_PUBLIC` | Usually, to be used in combination with the previous option. This generates assets without signatures, which basically means they don't expire. | `true` | | | `AWS_FORCE_PATH_STYLE` | Certain providers do not support the bucket name as the subdomain of the AWS endpoint (ie: Contabo). Set to `true` if that's the case. | `false` | | | `CONTENT_SECURITY_POLICY` | Sets custom Content Security Policy headers for enhanced security. When set, it is added to the default CSP configuration. | `""` (disabled) | | -| `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send and email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. Exact email addresses (without a leading `@`) must match the full recipient email, while domain patterns starting with `@` are matched as suffixes of the recipient email. Leave empty to disable any interception. | `""` | +| `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send an email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. Exact email addresses (without a leading `@`) must match the full recipient email, while domain patterns starting with `@` are matched as suffixes of the recipient email. Leave empty to disable any interception. | `""` | ## Installation From 5f8dd2e5ad4ce01cb79cab2e19bbe2da3014b7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Mon, 22 Dec 2025 16:14:13 +0100 Subject: [PATCH 5/6] add message --- app/overrides/decidim/add_staging_warning.rb | 8 ++++++ .../pokecode/_staging_warning.html.erb | 6 +++++ config/locales/en.yml | 7 ++--- lib/decidim/pokecode.rb | 2 ++ lib/decidim/pokecode/engine.rb | 2 +- .../decidim/pokecode/mail_interceptor.rb | 0 spec/lib/overrides_spec.rb | 6 +++++ spec/system/allowed_recipients_spec.rb | 27 +++++++++++++++++++ 8 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 app/overrides/decidim/add_staging_warning.rb create mode 100644 app/views/decidim/pokecode/_staging_warning.html.erb rename {app/mailers => lib}/decidim/pokecode/mail_interceptor.rb (100%) create mode 100644 spec/system/allowed_recipients_spec.rb diff --git a/app/overrides/decidim/add_staging_warning.rb b/app/overrides/decidim/add_staging_warning.rb new file mode 100644 index 0000000..07a241e --- /dev/null +++ b/app/overrides/decidim/add_staging_warning.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +if Decidim::Pokecode.allowed_recipients_list.any? + Deface::Override.new(:virtual_path => "decidim/admin/dashboard/show", + :name => "add-staging-warning", + :insert_before => "div.content", + :partial => "decidim/pokecode/staging_warning") +end diff --git a/app/views/decidim/pokecode/_staging_warning.html.erb b/app/views/decidim/pokecode/_staging_warning.html.erb new file mode 100644 index 0000000..69a9017 --- /dev/null +++ b/app/views/decidim/pokecode/_staging_warning.html.erb @@ -0,0 +1,6 @@ +
+

+ <%= t("decidim.pokecode.staging_warning.title") %>
+ <%= t("decidim.pokecode.staging_warning.message", emails: Decidim::Pokecode.allowed_recipients_list.map {|email| "#{email}"}.join(", ")).html_safe %> +

+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 232ff21..f893e0e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,7 @@ --- en: decidim: - components: - Pokecode: - name: Pokecode + pokecode: + staging_warning: + title: "Warning: Staging Environment" + message: "Emails sent from this instance may not reach their intended recipients. Allowed destinations are: %{emails}." diff --git a/lib/decidim/pokecode.rb b/lib/decidim/pokecode.rb index 2cf4bda..4e96e76 100644 --- a/lib/decidim/pokecode.rb +++ b/lib/decidim/pokecode.rb @@ -19,6 +19,8 @@ require "sidekiq/cron" end +require "decidim/pokecode/mail_interceptor" if Decidim::Pokecode.allowed_recipients_list.any? + require "decidim/pokecode/admin" require "decidim/pokecode/engine" require "decidim/pokecode/admin_engine" diff --git a/lib/decidim/pokecode/engine.rb b/lib/decidim/pokecode/engine.rb index 14799ef..04c1915 100644 --- a/lib/decidim/pokecode/engine.rb +++ b/lib/decidim/pokecode/engine.rb @@ -170,7 +170,7 @@ class Engine < ::Rails::Engine end initializer "pokecode.mail_interceptor" do - if Decidim::Pokecode.allowed_recipients_list.present? + if Decidim::Pokecode.allowed_recipients_list.any? Mail.register_interceptor(Decidim::Pokecode::MailInterceptor) Rails.logger.info "[Decidim::Pokecode] Email interceptor enabled. Allowed recipients: #{Decidim::Pokecode.allowed_recipients_list.join(", ")}" else diff --git a/app/mailers/decidim/pokecode/mail_interceptor.rb b/lib/decidim/pokecode/mail_interceptor.rb similarity index 100% rename from app/mailers/decidim/pokecode/mail_interceptor.rb rename to lib/decidim/pokecode/mail_interceptor.rb diff --git a/spec/lib/overrides_spec.rb b/spec/lib/overrides_spec.rb index ffd67e0..ac7422f 100644 --- a/spec/lib/overrides_spec.rb +++ b/spec/lib/overrides_spec.rb @@ -20,6 +20,12 @@ "/app/permissions/decidim/assemblies/permissions.rb" => "f26397a30c34eeb60af141b8ef0eb1bb" } }, + { + package: "decidim-admin", + files: { + "/app/views/decidim/admin/dashboard/show.html.erb" => "45558619f30212c2aa079e744c4be4ea" + } + }, { package: "aws-sdk-s3", files: { diff --git a/spec/system/allowed_recipients_spec.rb b/spec/system/allowed_recipients_spec.rb new file mode 100644 index 0000000..74256d4 --- /dev/null +++ b/spec/system/allowed_recipients_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "AllowedRecipients" do + let!(:organization) { create(:organization) } + let!(:user) { create(:user, :confirmed, :admin, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin.root_path + end + + if Decidim::Pokecode.allowed_recipients_list.any? + it "shows the staging warning in the admin dashboard" do + expect(page).to have_content("Warning: Staging Environment") + Decidim::Pokecode.allowed_recipients_list.each do |email| + expect(page).to have_content(email) + end + end + else + it "does not show the staging warning in the admin dashboard" do + expect(page).to have_no_content("Warning: Staging Environment") + end + end +end From 06bb1413196b5b7eb213e9d6a6c27fca22c12545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Mon, 22 Dec 2025 16:25:31 +0100 Subject: [PATCH 6/6] fix spec --- .github/test-env-configs.json | 18 ++++++++++++------ spec/mailers/mail_interceptor_spec.rb | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/test-env-configs.json b/.github/test-env-configs.json index da75e7b..88d25f8 100644 --- a/.github/test-env-configs.json +++ b/.github/test-env-configs.json @@ -11,7 +11,8 @@ "AWS_CDN_HOST": "https://cdn.example.com", "DISABLE_POKECODE_FOOTER": "", "DISABLE_LANGUAGE_MENU": "", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "", + "ALLOWED_RECIPIENTS": "" }, { "env-config": "all-disabled", @@ -25,7 +26,8 @@ "AWS_CDN_HOST": "", "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" }, { "env-config": "footer-only", @@ -39,7 +41,8 @@ "AWS_CDN_HOST": "", "DISABLE_POKECODE_FOOTER": "", "DISABLE_LANGUAGE_MENU": "1", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", + "ALLOWED_RECIPIENTS": "" }, { "env-config": "language-menu-only", @@ -53,7 +56,8 @@ "AWS_CDN_HOST": "", "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", + "ALLOWED_RECIPIENTS": "" }, { "env-config": "assembly-members-visible-only", @@ -67,7 +71,8 @@ "AWS_CDN_HOST": "", "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "", + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" }, { "env-config": "admin-iframe-only", @@ -81,6 +86,7 @@ "AWS_CDN_HOST": "", "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", - "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1" + "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" } ] diff --git a/spec/mailers/mail_interceptor_spec.rb b/spec/mailers/mail_interceptor_spec.rb index ce852d0..972c512 100644 --- a/spec/mailers/mail_interceptor_spec.rb +++ b/spec/mailers/mail_interceptor_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "spec_helper" +require "decidim/pokecode/mail_interceptor" module Decidim::Pokecode describe MailInterceptor do