feat(smtp): external SMTP config via DOCUSEAL_CONFIG_SMTP_*

pull/639/head
Bob Develop 2 weeks ago
parent 05e312bd14
commit fd953d71fe

@ -3,7 +3,15 @@
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Email SMTP</h1>
<% value = @encrypted_config.value || {} %>
<% external_smtp = ExternalConfig.smtp_configured? %>
<% if external_smtp %>
<div class="alert alert-info mb-4" role="status">
SMTP is configured via environment variables. These settings are read-only.
</div>
<% value = value.merge(ExternalConfig.smtp_settings.transform_keys { |k| { address: 'host', user_name: 'username' }.fetch(k, k.to_s) }.stringify_keys) %>
<% end %>
<%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<fieldset <%= 'disabled' if external_smtp %> class="space-y-4">
<%= f.fields_for :value do |ff| %>
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
@ -56,6 +64,7 @@
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
</fieldset>
<% end %>
</div>
<div class="w-0 md:w-52"></div>

@ -15,6 +15,17 @@ module ActionMailerConfigsInterceptor
return message
end
# External SMTP config via env vars takes precedence over EncryptedConfig.
if ExternalConfig.smtp_configured?
settings = ExternalConfig.smtp_settings
from = settings.delete(:from)
message.delivery_method(:smtp, settings)
message.from = from if from.present? && message[:from].blank?
return message
end
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
from = ENV.fetch('SMTP_FROM').to_s.split(',').sample

@ -0,0 +1,42 @@
# frozen_string_literal: true
# External configuration loaded from environment variables.
# Currently exposes SMTP settings; additional external config concerns can be
# added here as needed.
module ExternalConfig
SMTP_ENV_KEYS = {
address: 'DOCUSEAL_CONFIG_SMTP_ADDRESS',
port: 'DOCUSEAL_CONFIG_SMTP_PORT',
user_name: 'DOCUSEAL_CONFIG_SMTP_USERNAME',
password: 'DOCUSEAL_CONFIG_SMTP_PASSWORD',
domain: 'DOCUSEAL_CONFIG_SMTP_DOMAIN',
from: 'DOCUSEAL_CONFIG_SMTP_FROM'
}.freeze
module_function
# SMTP is considered configured as soon as an address is provided via ENV.
def smtp_configured?
ENV.fetch(SMTP_ENV_KEYS[:address], nil).present?
end
# Returns an ActionMailer-compatible SMTP settings hash built from ENV vars.
# The :from key is returned alongside but is intended for message[:from]
# rewriting, not for Net::SMTP.
def smtp_settings
return {} unless smtp_configured?
{
address: ENV.fetch(SMTP_ENV_KEYS[:address], nil),
port: ENV.fetch(SMTP_ENV_KEYS[:port], '587').to_i,
user_name: ENV.fetch(SMTP_ENV_KEYS[:user_name], nil),
password: ENV.fetch(SMTP_ENV_KEYS[:password], nil),
domain: ENV.fetch(SMTP_ENV_KEYS[:domain], nil),
from: ENV.fetch(SMTP_ENV_KEYS[:from], nil),
authentication: ENV.fetch(SMTP_ENV_KEYS[:password], nil).present? ? :plain : nil,
enable_starttls_auto: true,
open_timeout: ENV.fetch('SMTP_OPEN_TIMEOUT', '15').to_i,
read_timeout: ENV.fetch('SMTP_READ_TIMEOUT', '25').to_i
}.compact_blank
end
end

@ -0,0 +1,16 @@
import { test, expect } from '@playwright/test';
import { loginAsAdmin } from './helpers/auth';
// Phase 0.2 — External SMTP via env vars.
// Default state (no env override): form fields are editable.
test.describe('External SMTP', () => {
test('SMTP settings form is editable when no env override is set', async ({ page }) => {
await loginAsAdmin(page);
await page.goto('/settings/email');
const host = page.locator('input[name="encrypted_config[value][host]"]');
await expect(host).toBeVisible();
await expect(host).toBeEditable();
});
});

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'rails_helper'
require 'action_mailer_configs_interceptor'
RSpec.describe ActionMailerConfigsInterceptor do
let(:message) do
Mail.new do
to 'user@example.com'
from 'sender@example.com'
subject 'Hi'
body 'Hello'
end
end
describe '.delivering_email' do
before { allow(Rails.env).to receive(:production?).and_return(true) }
it 'applies env SMTP settings when ExternalConfig.smtp_configured?' do
envs = {
'DOCUSEAL_CONFIG_SMTP_ADDRESS' => 'smtp.example.com',
'DOCUSEAL_CONFIG_SMTP_PORT' => '2525',
'DOCUSEAL_CONFIG_SMTP_USERNAME' => 'user',
'DOCUSEAL_CONFIG_SMTP_PASSWORD' => 'secret'
}
with_env(envs) do
described_class.delivering_email(message)
method = message.delivery_method
expect(method.settings[:address]).to eq('smtp.example.com')
expect(method.settings[:port]).to eq(2525)
expect(method.settings[:user_name]).to eq('user')
end
end
it 'does not use env SMTP settings when env is absent' do
with_env('DOCUSEAL_CONFIG_SMTP_ADDRESS' => nil) do
# Falls through to the legacy branches; we only assert it did not pick env settings.
described_class.delivering_email(message)
method = message.delivery_method
expect(method.settings[:address]).not_to eq('smtp.example.com')
end
end
end
end

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'rails_helper'
require 'external_config'
RSpec.describe ExternalConfig do
describe '.smtp_configured?' do
it 'returns true when DOCUSEAL_CONFIG_SMTP_ADDRESS is set' do
with_env('DOCUSEAL_CONFIG_SMTP_ADDRESS' => 'smtp.example.com') do
expect(described_class.smtp_configured?).to be(true)
end
end
it 'returns false when the env var is absent' do
with_env('DOCUSEAL_CONFIG_SMTP_ADDRESS' => nil) do
expect(described_class.smtp_configured?).to be(false)
end
end
end
describe '.smtp_settings' do
it 'returns an empty hash when not configured' do
with_env('DOCUSEAL_CONFIG_SMTP_ADDRESS' => nil) do
expect(described_class.smtp_settings).to eq({})
end
end
it 'returns a hash built from env vars' do
envs = {
'DOCUSEAL_CONFIG_SMTP_ADDRESS' => 'smtp.example.com',
'DOCUSEAL_CONFIG_SMTP_PORT' => '2525',
'DOCUSEAL_CONFIG_SMTP_USERNAME' => 'user',
'DOCUSEAL_CONFIG_SMTP_PASSWORD' => 'secret',
'DOCUSEAL_CONFIG_SMTP_DOMAIN' => 'example.com',
'DOCUSEAL_CONFIG_SMTP_FROM' => 'noreply@example.com'
}
with_env(envs) do
settings = described_class.smtp_settings
expect(settings[:address]).to eq('smtp.example.com')
expect(settings[:port]).to eq(2525)
expect(settings[:user_name]).to eq('user')
expect(settings[:password]).to eq('secret')
expect(settings[:domain]).to eq('example.com')
expect(settings[:from]).to eq('noreply@example.com')
expect(settings[:authentication]).to eq(:plain)
end
end
end
end
Loading…
Cancel
Save