Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/test-env-configs.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"DISABLE_POKECODE_FOOTER": "",
"DISABLE_LANGUAGE_MENU": "",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "",
"ALLOWED_RECIPIENTS": ""
"ALLOWED_RECIPIENTS": "",
"DISABLE_INVITATIONS": ""
},
{
"env-config": "all-disabled",
Expand All @@ -27,7 +28,8 @@
"DISABLE_POKECODE_FOOTER": "1",
"DISABLE_LANGUAGE_MENU": "1",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1",
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com"
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com",
"DISABLE_INVITATIONS": "true"
},
{
"env-config": "footer-only",
Expand All @@ -42,7 +44,8 @@
"DISABLE_POKECODE_FOOTER": "",
"DISABLE_LANGUAGE_MENU": "1",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1",
"ALLOWED_RECIPIENTS": ""
"ALLOWED_RECIPIENTS": "",
"DISABLE_INVITATIONS": ""
},
{
"env-config": "language-menu-only",
Expand All @@ -57,7 +60,8 @@
"DISABLE_POKECODE_FOOTER": "1",
"DISABLE_LANGUAGE_MENU": "",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1",
"ALLOWED_RECIPIENTS": ""
"ALLOWED_RECIPIENTS": "",
"DISABLE_INVITATIONS": ""
},
{
"env-config": "assembly-members-visible-only",
Expand All @@ -72,7 +76,8 @@
"DISABLE_POKECODE_FOOTER": "1",
"DISABLE_LANGUAGE_MENU": "1",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "",
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com"
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com",
"DISABLE_INVITATIONS": ""
},
{
"env-config": "admin-iframe-only",
Expand All @@ -87,6 +92,7 @@
"DISABLE_POKECODE_FOOTER": "1",
"DISABLE_LANGUAGE_MENU": "1",
"DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1",
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com"
"ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com",
"DISABLE_INVITATIONS": ""
}
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This plugin relies on the command `decidim:upgrade` to make sure common files ar
| `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 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. | `""` |
| `DISABLE_INVITATIONS` | Prevents all invitation emails from being sent by intercepting emails with the `invitation-instructions` header. This is useful for development or testing environments. | `false` | |

## Installation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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
class AllowedRecipientsMailInterceptor
def self.delivering_email(message)
return if allowed_recipients_empty?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Decidim
module Pokecode
# Email interceptor that prevents invitation emails from being sent.
# Disables all emails that contain the 'invitation-instructions' header.
class DisableInvitationsMailInterceptor
def self.delivering_email(message)
return unless Decidim::Pokecode.disable_invitations

# Check if this is an invitation email
if message["invitation-instructions"].present?
Rails.logger.info "[Decidim::Pokecode] Invitation email prevented. Instructions: #{message["invitation-instructions"]}, To: #{message.to.join(", ")}"
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string interpolation in this log line uses double quotes both for the surrounding string and inside the #{...} block (message["invitation-instructions"]), which will cause a Ruby syntax error and prevent the file from loading. Please change the interpolation to avoid nested unescaped double quotes (for example by using a local variable or single-quoted key access) so the interceptor class is syntactically valid.

Suggested change
Rails.logger.info "[Decidim::Pokecode] Invitation email prevented. Instructions: #{message["invitation-instructions"]}, To: #{message.to.join(", ")}"
Rails.logger.info "[Decidim::Pokecode] Invitation email prevented. Instructions: #{message['invitation-instructions']}, To: #{message.to.join(", ")}"

Copilot uses AI. Check for mistakes.
message.perform_deliveries = false
end
end
end
end
end
8 changes: 0 additions & 8 deletions app/overrides/decidim/add_staging_warning.rb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="callout alert">
<p>
<strong><%= t("decidim.pokecode.invitations_disabled_warning.title") %></strong><br>
<%= t("decidim.pokecode.invitations_disabled_warning.message") %>
</p>
</div>
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ en:
staging_warning:
title: "Warning: Staging Environment"
message: "Emails sent from this instance may not reach their intended recipients. Allowed destinations are: %{emails}."
invitations_disabled_warning:
title: "Warning: Invitations Disabled"
message: "Invitation emails are currently disabled in this instance. Users will not receive invitation emails."
4 changes: 1 addition & 3 deletions lib/decidim/pokecode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
require "decidim/pokecode/configuration"
require "rails"
require "decidim/core"
require "deface"
require "health_check" if Decidim::Pokecode.health_check_enabled
require "rails_semantic_logger" if Decidim::Pokecode.semantic_logger_enabled
require "deface" if Decidim::Pokecode.deface_enabled
require "aws-sdk-s3" if Decidim::Pokecode.aws_cdn_host.present?
require "decidim/pokecode/s3_object_override" if Decidim::Pokecode.aws_cdn_host.present?

Expand All @@ -19,8 +19,6 @@
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"
8 changes: 4 additions & 4 deletions lib/decidim/pokecode/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ module Pokecode
Decidim::Env.new("ALLOWED_RECIPIENTS", "").value
end

config_accessor :disable_invitations do
Decidim::Env.new("DISABLE_INVITATIONS", false).present?
end

config_accessor :content_security_policies_extra do
{
"connect-src" => ENV.fetch("CONTENT_SECURITY_POLICY", "").split,
Expand All @@ -94,10 +98,6 @@ 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

def self.sentry_enabled
Pokecode.sentry_dsn.present?
end
Expand Down
41 changes: 35 additions & 6 deletions lib/decidim/pokecode/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ class Engine < ::Rails::Engine
else
Rails.logger.info "[Decidim::Pokecode] Active Storage CDN override disabled."
end

# Register deface overrides for admin dashboard warnings
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/admin/staging_warning")
Rails.logger.info "[Decidim::Pokecode] Staging warning deface override registered."
end

if Decidim::Pokecode.disable_invitations
Deface::Override.new(:virtual_path => "decidim/admin/dashboard/show",
:name => "add-invitations-disabled-warning",
:insert_before => "div.content",
:partial => "decidim/pokecode/admin/invitations_disabled_warning")
Rails.logger.info "[Decidim::Pokecode] Invitations disabled warning deface override registered."
end
end

initializer "pokecode.zeitwerk_ignore_deface" do
Expand Down Expand Up @@ -176,12 +193,24 @@ class Engine < ::Rails::Engine
end

initializer "pokecode.mail_interceptor" do
if Decidim::Pokecode.allowed_recipients_list.any?
config.action_mailer.interceptors ||= []
config.action_mailer.interceptors << "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."
Rails.application.config.to_prepare do
if Decidim::Pokecode.allowed_recipients_list.any?
unless ActionMailer::Base.try(:delivery_interceptors)&.include?(Decidim::Pokecode::AllowedRecipientsMailInterceptor)
ActionMailer::Base.register_interceptor(Decidim::Pokecode::AllowedRecipientsMailInterceptor)
end
Rails.logger.info "[Decidim::Pokecode] Allowed recipients mail interceptor enabled. Allowed recipients: #{Decidim::Pokecode.allowed_recipients_list.join(", ")}"
else
Rails.logger.info "[Decidim::Pokecode] Allowed recipients mail interceptor disabled."
end

if Decidim::Pokecode.disable_invitations
unless ActionMailer::Base.try(:delivery_interceptors)&.include?(Decidim::Pokecode::DisableInvitationsMailInterceptor)
ActionMailer::Base.register_interceptor(Decidim::Pokecode::DisableInvitationsMailInterceptor)
end
Rails.logger.info "[Decidim::Pokecode] Invitations disabled via mail interceptor."
else
Rails.logger.info "[Decidim::Pokecode] Invitations not disabled via mail interceptor."
end
end
end
end
Expand Down
10 changes: 0 additions & 10 deletions spec/lib/loaded_gems_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ module Decidim
end
end

if Decidim::Pokecode.deface_enabled
it "loads Deface" do
expect(defined?(Deface)).to be_truthy
end
else
it "does not load Deface" do
expect(defined?(Deface)).to be_falsey
end
end

if Decidim::Pokecode.aws_cdn_host.present?
it "loads Aws::S3" do
expect(Aws::S3::Object.included_modules).to include(Decidim::Pokecode::S3ObjectOverride)
Expand Down
75 changes: 75 additions & 0 deletions spec/mailers/disable_invitations_mail_interceptor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim::Pokecode
describe DisableInvitationsMailInterceptor do
let(:message) do
instance_double(
Mail::Message,
to: ["user@example.com"],
perform_deliveries: true
)
end

before do
allow(Rails.logger).to receive(:info)
allow(message).to receive(:perform_deliveries=)
allow(message).to receive(:[]).and_return(nil)
end

describe ".delivering_email" do
context "when disable_invitations is disabled" do
before do
allow(Decidim::Pokecode).to receive(:disable_invitations).and_return(false)
end

it "does not intercept any emails" do
allow(message).to receive(:[]).with("invitation-instructions").and_return("invite_private_user")
described_class.delivering_email(message)
expect(message).not_to have_received(:perform_deliveries=)
end
end

context "when disable_invitations is enabled" do
before do
allow(Decidim::Pokecode).to receive(:disable_invitations).and_return(true)
end

context "when the email has an invitation-instructions header" do
before do
allow(message).to receive(:[]).with("invitation-instructions").and_return("invite_private_user")
end

it "prevents the email from being delivered" 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(:info).with(
include("[Decidim::Pokecode] Invitation email prevented")
)
end
end

context "when the email does not have an invitation-instructions header" do
before do
allow(message).to receive(:[]).with("invitation-instructions").and_return(nil)
end

it "allows the email to be delivered" do
described_class.delivering_email(message)
expect(message).not_to have_received(:perform_deliveries=)
end

it "does not log the interception" do
described_class.delivering_email(message)
expect(Rails.logger).not_to have_received(:info)
end
end
end
end
end
end
3 changes: 1 addition & 2 deletions spec/mailers/mail_interceptor_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# frozen_string_literal: true

require "spec_helper"
require "decidim/pokecode/mail_interceptor"

module Decidim::Pokecode
describe MailInterceptor do
describe AllowedRecipientsMailInterceptor do
let(:message) do
instance_double(
Mail::Message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require "spec_helper"

describe "AllowedRecipients" do
describe "MailInterceptor" do
let!(:organization) { create(:organization) }
let!(:user) { create(:user, :confirmed, :admin, organization:) }

Expand All @@ -24,4 +24,15 @@
expect(page).to have_no_content("Warning: Staging Environment")
end
end

if Decidim::Pokecode.disable_invitations
it "shows the invitations disabled warning in the admin dashboard" do
expect(page).to have_content("Warning: Invitations Disabled")
expect(page).to have_content("Invitation emails are currently disabled in this instance. Users will not receive invitation emails.")
end
else
it "does not show the invitations disabled warning in the admin dashboard" do
expect(page).to have_no_content("Warning: Invitations Disabled")
end
end
end