Compare commits

..
39 changed files with 1205 additions and 1102 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 3.4
TargetRubyVersion: 3.0
NewCops: enable
Exclude:
- "bin/*"
@@ -135,7 +135,7 @@ Style/FrozenStringLiteralComment:
Enabled: true
SafeAutoCorrect: true
Naming/PredicatePrefix:
Naming/PredicateName:
Enabled: false
Layout/LineLength:
+1 -1
View File
@@ -1 +1 @@
3.4.10
3.4.6
+1 -1
View File
@@ -6,7 +6,7 @@ This doc explains how to go about running Postal in development to allow you to
You will need a MySQL database server to get started. Postal needs to be able to make databases within that server whenever new mail servers are created so the permissions that you use should be suitable for that.
You'll also need Ruby. Postal currently uses the Ruby version in `.ruby-version`. Install that using whichever version manager takes your fancy - rbenv, asdf, rvm etc.
You'll also need Ruby. Postal currently uses Ruby 3.2.2. Install that using whichever version manager takes your fancy - rbenv, asdf, rvm etc.
## Clone
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ruby:3.4.10-slim-bookworm AS base
FROM ruby:3.4.6-slim-bookworm AS base
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
+4 -9
View File
@@ -8,17 +8,12 @@ gem "bcrypt"
gem "chronic"
gem "domain_name"
gem "dotenv"
gem "execjs"
gem "dynamic_form"
gem "execjs", "~> 2.7", "< 2.8"
gem "gelf"
gem "haml"
gem "hashie"
gem "highline", require: false
# json 3.0 removed the `quirks_mode` option, which Active Support 7.2 still
# passes in both its JSON encoder and decoder. Nothing else constrains json, so
# without this pin Bundler resolves to 3.x and every `to_json` call raises
# ArgumentError. Active Support dropped those calls in Rails 8.1, so this pin
# can go when we move to 8.1.
gem "json", "< 3"
gem "jwt"
gem "kaminari"
gem "klogger-logger"
@@ -34,7 +29,7 @@ gem "ostruct"
gem "prometheus-client"
gem "puma"
gem "rackup"
gem "rails", "= 7.2.3.2"
gem "rails", "= 7.1.6"
gem "resolv"
gem "secure_headers"
gem "sentry-rails"
@@ -48,7 +43,7 @@ group :oidc do
gem "omniauth-rails_csrf_protection"
end
group :development, :test, :assets do
group :development, :assets do
gem "coffee-rails", "~> 5.0"
gem "jquery-rails"
gem "sass-rails"
+197 -209
View File
@@ -2,98 +2,104 @@ GEM
remote: https://rubygems.org/
specs:
abbrev (0.1.2)
actioncable (7.2.3.2)
actionpack (= 7.2.3.2)
activesupport (= 7.2.3.2)
actioncable (7.1.6)
actionpack (= 7.1.6)
activesupport (= 7.1.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.3.2)
actionpack (= 7.2.3.2)
activejob (= 7.2.3.2)
activerecord (= 7.2.3.2)
activestorage (= 7.2.3.2)
activesupport (= 7.2.3.2)
mail (>= 2.8.0)
actionmailer (7.2.3.2)
actionpack (= 7.2.3.2)
actionview (= 7.2.3.2)
activejob (= 7.2.3.2)
activesupport (= 7.2.3.2)
mail (>= 2.8.0)
actionmailbox (7.1.6)
actionpack (= 7.1.6)
activejob (= 7.1.6)
activerecord (= 7.1.6)
activestorage (= 7.1.6)
activesupport (= 7.1.6)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.6)
actionpack (= 7.1.6)
actionview (= 7.1.6)
activejob (= 7.1.6)
activesupport (= 7.1.6)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.2.3.2)
actionview (= 7.2.3.2)
activesupport (= 7.2.3.2)
actionpack (7.1.6)
actionview (= 7.1.6)
activesupport (= 7.1.6)
cgi
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.3)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.3.2)
actionpack (= 7.2.3.2)
activerecord (= 7.2.3.2)
activestorage (= 7.2.3.2)
activesupport (= 7.2.3.2)
actiontext (7.1.6)
actionpack (= 7.1.6)
activerecord (= 7.1.6)
activestorage (= 7.1.6)
activesupport (= 7.1.6)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.3.2)
activesupport (= 7.2.3.2)
actionview (7.1.6)
activesupport (= 7.1.6)
builder (~> 3.1)
cgi
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.3.2)
activesupport (= 7.2.3.2)
activejob (7.1.6)
activesupport (= 7.1.6)
globalid (>= 0.3.6)
activemodel (7.2.3.2)
activesupport (= 7.2.3.2)
activerecord (7.2.3.2)
activemodel (= 7.2.3.2)
activesupport (= 7.2.3.2)
activemodel (7.1.6)
activesupport (= 7.1.6)
activerecord (7.1.6)
activemodel (= 7.1.6)
activesupport (= 7.1.6)
timeout (>= 0.4.0)
activestorage (7.2.3.2)
actionpack (= 7.2.3.2)
activejob (= 7.2.3.2)
activerecord (= 7.2.3.2)
activesupport (= 7.2.3.2)
activestorage (7.1.6)
actionpack (= 7.1.6)
activejob (= 7.1.6)
activerecord (= 7.1.6)
activesupport (= 7.1.6)
marcel (~> 1.0)
activesupport (7.2.3.2)
activesupport (7.1.6)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1, < 6)
minitest (>= 5.1)
mutex_m
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.3)
ast (2.4.2)
attr_required (1.0.2)
authie (5.0.0)
activerecord (>= 6.1, < 9.0)
autoprefixer-rails (10.4.21.0)
autoprefixer-rails (10.4.13.0)
execjs (~> 2)
base64 (0.3.0)
bcrypt (3.1.22)
bcrypt (3.1.20)
benchmark (0.5.0)
bigdecimal (4.1.3)
bindata (3.0.0)
bigdecimal (4.1.2)
bindata (2.5.0)
builder (3.3.0)
cgi (0.5.2)
cgi (0.5.1)
chronic (0.10.2)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
@@ -102,74 +108,71 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.8)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crack (1.0.1)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.7)
crass (1.0.6)
database_cleaner-active_record (2.2.2)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.1.0)
database_cleaner-core (2.0.1)
date (3.5.1)
diff-lcs (1.6.2)
domain_name (0.6.20260907)
dotenv (3.2.0)
domain_name (0.6.20240107)
dotenv (3.0.2)
drb (2.2.3)
erb (6.0.7)
dynamic_form (1.2.0)
email_validator (2.2.4)
activemodel
erb (6.0.4)
erubi (1.13.1)
execjs (2.10.2)
json (>= 2)
factory_bot (6.6.0)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.5.0)
execjs (2.7.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.4)
net-http (~> 0.5)
faraday-net_http (3.1.0)
net-http
ffi (1.15.5)
gelf (3.1.0)
json
globalid (1.4.0)
globalid (1.3.0)
activesupport (>= 6.1)
haml (7.5.1)
prism (>= 1.1.0)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
hashdiff (1.2.1)
hashie (5.1.0)
logger
highline (3.1.2)
reline
i18n (1.15.2)
hashdiff (1.1.0)
hashie (5.0.0)
highline (2.1.0)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
io-console (0.9.2)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jquery-rails (4.6.1)
jquery-rails (4.5.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.21.2)
json-jwt (1.17.2)
json (2.7.1)
json-jwt (1.16.6)
activesupport (>= 4.2)
aes_key_wrap
base64
bindata
faraday (~> 2.0)
faraday-follow_redirects
jwt (3.3.0)
jwt (2.8.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@@ -183,19 +186,17 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
klogger-logger (1.5.0)
klogger-logger (1.4.0)
concurrent-ruby (>= 1.0, < 2.0)
json
rouge (>= 3.30, < 5.0)
konfig-config (3.0.0)
hashie
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.25.2)
loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
@@ -203,18 +204,19 @@ GEM
net-smtp
marcel (1.2.1)
mini_mime (1.1.5)
minitest (5.27.0)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
mutex_m (0.3.0)
mysql2 (0.5.7)
bigdecimal
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.7)
mysql2 (0.5.6)
net-http (0.4.1)
uri
net-imap (0.6.4)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.3.0)
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
@@ -223,28 +225,28 @@ GEM
activerecord (>= 4.0.0)
activesupport (>= 4.0.0)
nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu)
nokogiri (1.19.3-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-arm64-darwin)
nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-darwin)
nokogiri (1.19.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
omniauth (2.1.4)
omniauth (2.1.2)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (2.0.1)
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth_openid_connect (0.8.0)
omniauth_openid_connect (0.7.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.5.0)
openid_connect (2.3.0)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
@@ -255,29 +257,30 @@ GEM
validate_url
webfinger (~> 2.0)
ostruct (0.6.3)
parallel (2.2.0)
parser (3.3.12.0)
parallel (1.22.1)
parser (3.2.1.1)
ast (~> 2.4.1)
racc
pp (0.6.4)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
prometheus-client (5.0.0)
base64
public_suffix (7.0.5)
puma (8.0.2)
prometheus-client (4.2.2)
psych (5.4.0)
date
stringio
public_suffix (5.0.4)
puma (7.0.4)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.7)
rack-oauth2 (2.3.0)
rack (3.2.6)
rack-oauth2 (2.2.1)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.2.1)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
@@ -288,32 +291,32 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (7.2.3.2)
actioncable (= 7.2.3.2)
actionmailbox (= 7.2.3.2)
actionmailer (= 7.2.3.2)
actionpack (= 7.2.3.2)
actiontext (= 7.2.3.2)
actionview (= 7.2.3.2)
activejob (= 7.2.3.2)
activemodel (= 7.2.3.2)
activerecord (= 7.2.3.2)
activestorage (= 7.2.3.2)
activesupport (= 7.2.3.2)
rails (7.1.6)
actioncable (= 7.1.6)
actionmailbox (= 7.1.6)
actionmailer (= 7.1.6)
actionpack (= 7.1.6)
actiontext (= 7.1.6)
actionview (= 7.1.6)
activejob (= 7.1.6)
activemodel (= 7.1.6)
activerecord (= 7.1.6)
activestorage (= 7.1.6)
activesupport (= 7.1.6)
bundler (>= 1.15.0)
railties (= 7.2.3.2)
railties (= 7.1.6)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (7.2.3.2)
actionpack (= 7.2.3.2)
activesupport (= 7.2.3.2)
railties (7.1.6)
actionpack (= 7.1.6)
activesupport (= 7.1.6)
cgi
irb (~> 1.13)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
@@ -321,62 +324,53 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.4.2)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
rdoc (7.2.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
psych (>= 4.0.0)
tsort
regexp_parser (2.12.0)
reline (0.7.0)
regexp_parser (2.7.0)
reline (0.6.3)
io-console (~> 0.5)
resolv (0.7.2)
resolv (0.6.2)
rexml (3.4.4)
rouge (4.7.0)
rspec (3.13.2)
rouge (4.2.0)
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.8)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.4)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
rspec-core (>= 3.13.0, < 5.0.0)
rspec-expectations (>= 3.13.0, < 5.0.0)
rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7)
rubocop (1.91.0)
json (>= 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.13.6)
rubocop (1.48.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.50.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-rails (2.37.0)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.27.0)
parser (>= 3.2.1.0)
rubocop-rails (2.18.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.89.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop (>= 1.33.0, < 2.0)
ruby-progressbar (1.13.0)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
@@ -388,35 +382,32 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
secure_headers (7.3.0)
cgi (>= 0.1)
secure_headers (6.5.0)
securerandom (0.4.1)
sentry-rails (7.0.0)
railties (>= 5.2.0)
sentry-ruby (~> 7.0.0)
sentry-ruby (7.0.0)
bigdecimal
sentry-rails (5.16.1)
railties (>= 5.0)
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
logger
shoulda-matchers (8.0.1)
activesupport (>= 7.2)
sprockets (4.4.1)
concurrent-ruby (~> 1.1)
logger
shoulda-matchers (6.1.0)
activesupport (>= 5.2.0)
sprockets (4.2.0)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stringio (3.2.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
temple (0.10.7)
temple (0.10.3)
thor (1.5.0)
tilt (2.9.0)
timecop (0.9.11)
tilt (2.3.0)
timecop (0.9.8)
timeout (0.6.1)
tsort (0.2.0)
turbolinks (5.2.1)
@@ -424,13 +415,10 @@ GEM
turbolinks-source (5.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uglifier (4.2.1)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
useragent (0.16.11)
unicode-display_width (2.4.2)
uri (1.0.3)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
@@ -438,16 +426,16 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.26.4)
webmock (3.20.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
websocket-driver (0.8.2)
webrick (1.9.1)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.8.3)
zeitwerk (2.8.2)
PLATFORMS
aarch64-linux
@@ -466,14 +454,14 @@ DEPENDENCIES
database_cleaner-active_record
domain_name
dotenv
execjs
dynamic_form
execjs (~> 2.7, < 2.8)
factory_bot_rails
gelf
haml
hashie
highline
jquery-rails
json (< 3)
jwt
kaminari
klogger-logger
@@ -491,7 +479,7 @@ DEPENDENCIES
prometheus-client
puma
rackup
rails (= 7.2.3.2)
rails (= 7.1.6)
resolv
rspec
rspec-rails
@@ -9,6 +9,14 @@
line-height:1.5;
}
.errorExplanation h2 {
display:none;
}
.errorExplanation p {
display:none;
}
.errorExplanation ul li {
list-style:disc;
margin-left:20px;
+40 -1
View File
@@ -98,9 +98,48 @@ class DomainsController < ApplicationController
redirect_to [:verify, organization, @server, @domain], alert: "You can't set up DNS for this domain until it has been verified."
end
def regenerate_dkim
if @domain.pending_dkim_key?
redirect_to_with_json [:setup, organization, @server, @domain],
alert: "A DKIM key change is already in progress for #{@domain.name}. Publish the new " \
"record shown below, or cancel the change, before generating another key."
return
end
was_verified = @domain.dkim_status == "OK"
@domain.regenerate_dkim_key!
if was_verified
redirect_to_with_json [:setup, organization, @server, @domain],
notice: "A new DKIM key has been generated for #{@domain.name}. Add the new DNS record below — " \
"your existing key will remain active until the new record has been verified. Keep the " \
"old DNS record published afterward while messages signed with it may still be queued, " \
"held, or available for redelivery."
else
redirect_to_with_json [:setup, organization, @server, @domain], notice: "A new DKIM key has been generated for #{@domain.name}. Update your DKIM DNS record with the new value below."
end
end
def cancel_dkim_regeneration
@domain.cancel_pending_dkim_key!
redirect_to_with_json [:setup, organization, @server, @domain], notice: "The pending DKIM key for #{@domain.name} has been discarded. Your existing key remains active."
end
def check
had_pending_dkim_key = @domain.pending_dkim_key?
previous_dkim_record_name = @domain.dkim_record_name
if @domain.check_dns(:manual)
redirect_to_with_json [organization, @server, :domains], notice: "Your DNS records for #{@domain.name} look good!"
if had_pending_dkim_key && !@domain.pending_dkim_key?
redirect_to_with_json [:setup, organization, @server, @domain],
notice: "Your DNS records for #{@domain.name} look good! Your new DKIM key is now active. " \
"Keep the old record at #{previous_dkim_record_name} published while messages " \
"signed with the old key may still be queued, held, or available for redelivery."
elsif @domain.pending_dkim_key?
redirect_to_with_json [:setup, organization, @server, @domain],
alert: "We couldn't verify the record for your new DKIM key yet. Your existing key remains " \
"active. Check below for the expected record details."
else
redirect_to_with_json [organization, @server, :domains], notice: "Your DNS records for #{@domain.name} look good!"
end
else
redirect_to_with_json [:setup, organization, @server, @domain], alert: "There seems to be something wrong with your DNS records. Check below for information."
end
-20
View File
@@ -1,20 +0,0 @@
# frozen_string_literal: true
module ErrorMessagesHelper
# Renders a record's validation errors.
#
# This replaces the helper of the same name from the dynamic_form gem, which
# is unmaintained and cannot be installed on Ruby 3.4. The markup matches what
# that gem produced, minus the heading and description which the
# errorExplanation styles have always hidden, and matches the structure
# ajax.coffee builds when it renders errors from a remote form.
def error_messages_for(record)
record = record.to_model if record.respond_to?(:to_model)
return "".html_safe if record.nil? || record.errors.empty?
items = safe_join(record.errors.full_messages.map { |message| tag.li(message) })
tag.div(tag.ul(items), id: "errorExplanation", class: "errorExplanation")
end
end
+21
View File
@@ -72,6 +72,17 @@ module HasDNSChecks
#
def check_dkim_record
# If a new DKIM key is waiting to be activated, promote it to be the active
# key as soon as its DNS record is in place. The matching DNS response has
# already verified the key, so avoid a second lookup which could return a
# different answer while DNS changes are propagating.
if pending_dkim_key? && pending_dkim_record_live?
activate_pending_dkim_key
self.dkim_status = "OK"
self.dkim_error = nil
return true
end
domain = "#{dkim_record_name}.#{name}"
records = resolver.txt(domain)
if records.empty?
@@ -98,6 +109,16 @@ module HasDNSChecks
save!
end
# Returns true if the DNS record for the pending DKIM key has been published
# correctly.
def pending_dkim_record_live?
records = resolver.txt("#{pending_dkim_record_name}.#{name}")
return false unless records.size == 1
sanitised_record = records.first.strip.ends_with?(";") ? records.first.strip : "#{records.first.strip};"
sanitised_record == pending_dkim_record
end
#
# MX
#
+1 -1
View File
@@ -31,7 +31,7 @@ class Credential < ApplicationRecord
validate :validate_key_cannot_be_changed
validate :validate_key_for_smtp_ip
serialize :options, type: Hash, coder: YAML
serialize :options, type: Hash
before_validation :generate_key
+123 -36
View File
@@ -4,31 +4,33 @@
#
# Table name: domains
#
# id :integer not null, primary key
# server_id :integer
# uuid :string(255)
# name :string(255)
# verification_token :string(255)
# verification_method :string(255)
# verified_at :datetime
# dkim_private_key :text(65535)
# created_at :datetime
# updated_at :datetime
# dns_checked_at :datetime
# spf_status :string(255)
# spf_error :string(255)
# dkim_status :string(255)
# dkim_error :string(255)
# mx_status :string(255)
# mx_error :string(255)
# return_path_status :string(255)
# return_path_error :string(255)
# outgoing :boolean default(TRUE)
# incoming :boolean default(TRUE)
# owner_type :string(255)
# owner_id :integer
# dkim_identifier_string :string(255)
# use_for_any :boolean
# id :integer not null, primary key
# dkim_error :string(255)
# dkim_identifier_string :string(255)
# dkim_private_key :text(65535)
# dkim_status :string(255)
# dns_checked_at :datetime
# incoming :boolean default(TRUE)
# mx_error :string(255)
# mx_status :string(255)
# name :string(255)
# outgoing :boolean default(TRUE)
# owner_type :string(255)
# pending_dkim_identifier_string :string(255)
# pending_dkim_private_key :text(65535)
# return_path_error :string(255)
# return_path_status :string(255)
# spf_error :string(255)
# spf_status :string(255)
# use_for_any :boolean
# uuid :string(255)
# verification_method :string(255)
# verification_token :string(255)
# verified_at :datetime
# created_at :datetime
# updated_at :datetime
# owner_id :integer
# server_id :integer
#
# Indexes
#
@@ -82,7 +84,7 @@ class Domain < ApplicationRecord
end
def generate_dkim_key
self.dkim_private_key = OpenSSL::PKey::RSA.new(1024).to_s
self.dkim_private_key = OpenSSL::PKey::RSA.new(Postal::Config.dns.dkim_key_size).to_s
end
def dkim_key
@@ -91,6 +93,57 @@ class Domain < ApplicationRecord
@dkim_key ||= OpenSSL::PKey::RSA.new(dkim_private_key)
end
def pending_dkim_key
return nil unless pending_dkim_private_key
@pending_dkim_key ||= OpenSSL::PKey::RSA.new(pending_dkim_private_key)
end
def pending_dkim_key?
pending_dkim_private_key.present?
end
# Generate a new DKIM key for this domain. If the current key is verified and
# in use for signing, the new key is stored as a pending key under a new
# identifier so that signing continues with the current key until the new
# key's DNS record has been published and verified. Otherwise, the key is
# replaced immediately and any in-flight key change is abandoned, since the
# new key supersedes it.
def regenerate_dkim_key!
if dkim_status == "OK"
self.pending_dkim_private_key = OpenSSL::PKey::RSA.new(Postal::Config.dns.dkim_key_size).to_s
self.pending_dkim_identifier_string = generate_unique_dkim_identifier_string
else
generate_dkim_key
self.dkim_status = nil
self.dkim_error = nil
self.pending_dkim_private_key = nil
self.pending_dkim_identifier_string = nil
@dkim_key = nil
end
@pending_dkim_key = nil
save!
end
# Promote the pending DKIM key to be the active key. Does not save the record.
def activate_pending_dkim_key
return unless pending_dkim_key?
self.dkim_private_key = pending_dkim_private_key
self.dkim_identifier_string = pending_dkim_identifier_string
self.pending_dkim_private_key = nil
self.pending_dkim_identifier_string = nil
@dkim_key = nil
@pending_dkim_key = nil
end
def cancel_pending_dkim_key!
self.pending_dkim_private_key = nil
self.pending_dkim_identifier_string = nil
@pending_dkim_key = nil
save!
end
def to_param
uuid
end
@@ -108,23 +161,27 @@ class Domain < ApplicationRecord
end
def dkim_record
return if dkim_key.nil?
public_key = dkim_key.public_key.to_s.gsub(/-+[A-Z ]+-+\n/, "").gsub(/\n/, "")
"v=DKIM1; t=s; h=sha256; p=#{public_key};"
build_dkim_record(dkim_key)
end
def dkim_identifier
return nil unless dkim_identifier_string
Postal::Config.dns.dkim_identifier + "-#{dkim_identifier_string}"
build_dkim_identifier(dkim_identifier_string)
end
def dkim_record_name
identifier = dkim_identifier
return if identifier.nil?
build_dkim_record_name(dkim_identifier)
end
"#{identifier}._domainkey"
def pending_dkim_record
build_dkim_record(pending_dkim_key)
end
def pending_dkim_identifier
build_dkim_identifier(pending_dkim_identifier_string)
end
def pending_dkim_record_name
build_dkim_record_name(pending_dkim_identifier)
end
def return_path_domain
@@ -160,6 +217,36 @@ class Domain < ApplicationRecord
private
def build_dkim_record(key)
return if key.nil?
public_key = key.public_key.to_s.gsub(/-+[A-Z ]+-+\n/, "").gsub(/\n/, "")
"v=DKIM1; t=s; h=sha256; p=#{public_key};"
end
def build_dkim_identifier(identifier_string)
return nil unless identifier_string
Postal::Config.dns.dkim_identifier + "-#{identifier_string}"
end
def build_dkim_record_name(identifier)
return if identifier.nil?
"#{identifier}._domainkey"
end
# Generates a random identifier string in the same format as the
# `random_string :dkim_identifier_string` declaration, unique across both the
# active and pending identifier columns.
def generate_unique_dkim_identifier_string
loop do
string = Nifty::Utils::RandomString.generate(length: 6, upper_letters_only: true)
scope = self.class.where(dkim_identifier_string: string).or(self.class.where(pending_dkim_identifier_string: string))
return string unless scope.exists?
end
end
def update_verification_token_on_method_change
return unless verification_method_changed?
+1 -1
View File
@@ -34,7 +34,7 @@ class WebhookRequest < ApplicationRecord
validates :url, presence: true
validates :event, presence: true
serialize :payload, type: Hash, coder: YAML
serialize :payload, type: Hash
class << self
+2
View File
@@ -34,6 +34,8 @@
= link_to domain.name, [:setup, organization, @server, domain]
- if domain.use_for_any?
%span.label.label--blue Any
- if domain.pending_dkim_key?
= link_to "New DKIM key pending", [:setup, organization, @server, domain], :class => "label label--orange", :title => "A new DKIM key is waiting for its DNS record to be published."
%ul.domainList__checks
- if domain.spf_status == 'OK'
%li.domainList__check.domainList__check--ok SPF
+33 -5
View File
@@ -47,7 +47,11 @@
%pre.codeBlock.u-margin= @domain.spf_record
%h3.pageContent__subTitle DKIM Record
- if @domain.dkim_status == 'OK'
- if @domain.pending_dkim_key?
%p.pageContent__text.u-orange.u-bold
%span.label.label--orange Key change in progress
You've generated a new DKIM key which isn't active yet. Both records are shown below.
- elsif @domain.dkim_status == 'OK'
%p.pageContent__text.u-green.u-bold
%span.label.label--green Good
Your DKIM record looks good!
@@ -56,10 +60,34 @@
%span.label.label--orange Warning
= @domain.dkim_error
%p.pageContent__text
You need to add a new TXT record with the name <b>#{@domain.dkim_record_name}</b>
with the following content.
%pre.codeBlock.u-margin= @domain.dkim_record
- if @domain.pending_dkim_key?
%p.pageContent__text.u-bold
%span.label.label--green Current
Keep this record exactly as it is
%p.pageContent__text
This is the record you already have published at <b>#{@domain.dkim_record_name}</b>.
It's signing your mail right now, so leave it in place.
%pre.codeBlock.u-margin= @domain.dkim_record
%p.pageContent__text.u-bold
%span.label.label--orange New
Add this record as well
%p.pageContent__text
Add a <b>second</b> TXT record with the name <b>#{@domain.pending_dkim_record_name}</b>
and the content below, alongside the record above rather than replacing it. Once we detect
it (checked hourly, or when you use the button above) we'll switch to signing with the new
key automatically. After the switch, keep the old record published while messages signed
with it may still be queued, held, or available for redelivery.
%pre.codeBlock.u-margin= @domain.pending_dkim_record
%p.pageContent__text
= link_to "Cancel new DKIM key", [:cancel_dkim_regeneration, organization, @server, @domain], :remote => true, :method => :post, :data => { :confirm => "Are you sure? The new key will be discarded and your existing key will remain active." }, :class => 'button button--neutral'
- else
%p.pageContent__text
You need to add a new TXT record with the name <b>#{@domain.dkim_record_name}</b>
with the following content.
%pre.codeBlock.u-margin= @domain.dkim_record
%p.pageContent__text
= link_to "Regenerate DKIM key", [:regenerate_dkim, organization, @server, @domain], :remote => true, :method => :post, :data => { :confirm => "Are you sure you wish to generate a new DKIM key? You'll need to add a new DNS record before the new key can be used." }, :class => 'button button--neutral'
%h3.pageContent__subTitle Return Path
- if @domain.return_path_status == 'OK'
+1 -1
View File
@@ -25,7 +25,7 @@ Rails.application.configure do
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = :none
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
@@ -1,11 +0,0 @@
# frozen_string_literal: true
# Provides `f.error_messages` on form builders. This used to come from the
# dynamic_form gem - see ErrorMessagesHelper for why that was removed.
ActiveSupport.on_load(:action_view) do
ActionView::Helpers::FormBuilder.class_eval do
def error_messages
@template.error_messages_for(@object)
end
end
end
@@ -0,0 +1,142 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
#
# This file eases your Rails 7.0 framework defaults upgrade.
#
# Uncomment each configuration one by one to switch to the new default.
# Once your application is ready to run with all new defaults, you can remove
# this file and set the `config.load_defaults` to `7.0`.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
# `button_to` view helper will render `<button>` element, regardless of whether
# or not the content is passed as the first argument or as a block.
# Rails.application.config.action_view.button_to_generates_button_tag = true
# `stylesheet_link_tag` view helper will not render the media attribute by default.
# Rails.application.config.action_view.apply_stylesheet_media_default = false
# Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
# Changing this default means invalidate all encrypted messages generated by
# your application and, all the encrypted cookies. Only change this after you
# rotated all the messages using the key rotator.
#
# See upgrading guide for more information on how to build a rotator.
# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
# Change the digest class for ActiveSupport::Digest.
# Changing this default means that for example Etags change and
# various cache keys leading to cache invalidation.
# Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
# Don't override ActiveSupport::TimeWithZone.name and use the default Ruby
# implementation.
# Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
# Calls `Rails.application.executor.wrap` around test cases.
# This makes test cases behave closer to an actual request or job.
# Several features that are normally disabled in test, such as Active Record query cache
# and asynchronous queries will then be enabled.
# Rails.application.config.active_support.executor_around_test_case = true
# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
# Rails.application.config.action_mailer.smtp_timeout = 5
# The ActiveStorage video previewer will now use scene change detection to generate
# better preview images (rather than the previous default of using the first frame
# of the video).
# Rails.application.config.active_storage.video_preview_arguments =
# "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
# Automatically infer `inverse_of` for associations with a scope.
# Rails.application.config.active_record.automatic_scope_inversing = true
# Raise when running tests if fixtures contained foreign key violations
# Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
# Disable partial inserts.
# This default means that all columns will be referenced in INSERT queries
# regardless of whether they have a default or not.
# Rails.application.config.active_record.partial_inserts = false
# Protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`.
# Rails.application.config.action_controller.raise_on_open_redirects = true
# Change the variant processor for Active Storage.
# Changing this default means updating all places in your code that
# generate variants to use image processing macros and ruby-vips
# operations. See the upgrading guide for detail on the changes required.
# The `:mini_magick` option is not deprecated; it's fine to keep using it.
# Rails.application.config.active_storage.variant_processor = :vips
# Enable parameter wrapping for JSON.
# Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
# To disable parameter wrapping entirely, set this config to `false`.
# Rails.application.config.action_controller.wrap_parameters_by_default = true
# Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
# `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
#
# See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
# more information.
# Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
# Change the default headers to disable browsers' flawed legacy XSS protection.
# Rails.application.config.action_dispatch.default_headers = {
# "X-Frame-Options" => "SAMEORIGIN",
# "X-XSS-Protection" => "0",
# "X-Content-Type-Options" => "nosniff",
# "X-Download-Options" => "noopen",
# "X-Permitted-Cross-Domain-Policies" => "none",
# "Referrer-Policy" => "strict-origin-when-cross-origin"
# }
# ** Please read carefully, this must be configured in config/application.rb **
# Change the format of the cache entry.
# Changing this default means that all new cache entries added to the cache
# will have a different format that is not supported by Rails 6.1 applications.
# Only change this value after your application is fully deployed to Rails 7.0
# and you have no plans to rollback.
# When you're ready to change format, add this to `config/application.rb` (NOT this file):
# config.active_support.cache_format_version = 7.0
# Cookie serializer: 2 options
#
# If you're upgrading and haven't set `cookies_serializer` previously, your cookie serializer
# is `:marshal`. The default for new apps is `:json`.
#
# Rails.application.config.action_dispatch.cookies_serializer = :json
#
#
# To migrate an existing application to the `:json` serializer, use the `:hybrid` option.
#
# Rails transparently deserializes existing (Marshal-serialized) cookies on read and
# re-writes them in the JSON format.
#
# It is fine to use `:hybrid` long term; you should do that until you're confident *all* your cookies
# have been converted to JSON. To keep using `:hybrid` long term, move this config to its own
# initializer or to `config/application.rb`.
#
# Rails.application.config.action_dispatch.cookies_serializer = :hybrid
#
#
# If your cookies can't yet be serialized to JSON, keep using `:marshal` for backward-compatibility.
#
# If you have configured the serializer elsewhere, you can remove this section of the file.
#
# See https://guides.rubyonrails.org/action_controller_overview.html#cookies for more information.
# Change the return value of `ActionDispatch::Request#content_type` to the Content-Type header without modification.
# Rails.application.config.action_dispatch.return_only_request_media_type_on_content_type = false
# Active Storage `has_many_attached` relationships will default to replacing the current collection instead of appending to it.
# Thus, to support submitting an empty collection, the `file_field` helper will render an hidden field `include_hidden` by default when `multiple_file_field_include_hidden` is set to `true`.
# See https://guides.rubyonrails.org/configuring.html#config-active-storage-multiple-file-field-include-hidden for more information.
# Rails.application.config.active_storage.multiple_file_field_include_hidden = true
# ** Please read carefully, this must be configured in config/application.rb (NOT this file) **
# Disables the deprecated #to_s override in some Ruby core classes
# See https://guides.rubyonrails.org/configuring.html#config-active-support-disable-to-s-conversion for more information.
# config.active_support.disable_to_s_conversion = true
@@ -1,200 +0,0 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
#
# This file eases your Rails 7.1 framework defaults upgrade.
#
# Uncomment each configuration one by one to switch to the new default.
# Once your application is ready to run with all new defaults, you can remove
# this file and set the `config.load_defaults` to `7.1`.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
###
# No longer add autoloaded paths into `$LOAD_PATH`. This means that you won't be able
# to manually require files that are managed by the autoloader, which you shouldn't do anyway.
#
# This will reduce the size of the load path, making `require` faster if you don't use bootsnap, or reduce the size
# of the bootsnap cache if you use it.
#++
# Rails.application.config.add_autoload_paths_to_load_path = false
###
# Remove the default X-Download-Options header since it is used only by Internet Explorer.
# If you need to support Internet Explorer, add back `"X-Download-Options" => "noopen"`.
#++
# Rails.application.config.action_dispatch.default_headers = {
# "X-Frame-Options" => "SAMEORIGIN",
# "X-XSS-Protection" => "0",
# "X-Content-Type-Options" => "nosniff",
# "X-Permitted-Cross-Domain-Policies" => "none",
# "Referrer-Policy" => "strict-origin-when-cross-origin"
# }
###
# Do not treat an `ActiveSupport::Duration` as a number of seconds when it is
# used as a cache expiry. Raise instead, because the intent is ambiguous.
#++
# Rails.application.config.active_support.raise_on_invalid_cache_expiration_time = true
###
# Specify the default serializer used by `MessageEncryptor` and `MessageVerifier`
# instances.
#
# The legacy default is `:marshal`, which is a potential vector for
# deserialization attacks in cases where a message signing secret has been
# leaked.
#
# In Rails 7.1, the new default is `:json_allow_marshal`, which serializes and
# deserializes with `ActiveSupport::JSON`, but can fall back to deserializing
# with `Marshal` so that legacy messages can still be read.
#
# In Rails 7.2, the default will become `:json`, which serializes and
# deserializes with `ActiveSupport::JSON` only.
#
# Alternatively, you can choose `:message_pack` or `:message_pack_allow_marshal`,
# which serialize with `ActiveSupport::MessagePack`. `ActiveSupport::MessagePack`
# can roundtrip some Ruby types that are not supported by JSON, and may provide
# improved performance, but it requires the `msgpack` gem.
#
# For more information, see
# https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer
#
# NOTE: Changing this affects the session cookie and any other signed or
# encrypted message. Deploy `:json_allow_marshal` everywhere before considering
# `:json`, and be aware that rolling back to a version which cannot read the new
# format will invalidate existing sessions.
#++
# Rails.application.config.active_support.message_serializer = :json_allow_marshal
###
# Enable a performance optimization that serializes message data and metadata
# together. This changes the message format, so messages serialized this way
# cannot be read by older versions of Rails. However, messages that use the old
# format can still be read, regardless of whether this optimization is enabled.
#
# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have
# not yet been upgraded must be able to read messages from upgraded servers, leave
# this optimization off on the first deploy, then enable it on a subsequent deploy.
#++
# Rails.application.config.active_support.use_message_serializer_for_metadata = true
###
# Set the maximum size for Rails log files.
#
# `config.load_defaults "7.1"` does not set this value for environments other than
# development and test.
#++
# if Rails.env.local?
# Rails.application.config.log_file_size = 100 * 1024 * 1024
# end
###
# Enable raising on assignment to attr_readonly attributes. The previous behavior
# would allow assignment but silently not persist changes to the database.
#++
# Rails.application.config.active_record.raise_on_assign_to_attr_readonly = true
###
# Enable validating only parent-related columns for presence when the parent is mandatory.
# The previous behavior was to validate the presence of the parent record, which performed an extra query
# to get the parent every time the child record was updated, even when parent has not changed.
#++
# Rails.application.config.active_record.belongs_to_required_validates_foreign_key = false
###
# Enable preloading of the `before_committed!` callbacks on all records in a transaction,
# instead of only the first copy of a record.
#++
# Rails.application.config.active_record.before_committed_on_all_records = true
###
# Disable deprecated singular associations names.
#++
# Rails.application.config.active_record.allow_deprecated_singular_associations_name = false
###
# Enable running `after_commit` callbacks in the order they are defined,
# rather than in reverse order.
#++
# Rails.application.config.active_record.run_after_transaction_callbacks_in_order_defined = true
###
# Run `after_commit` and `after_rollback` callbacks on the first saved instance
# of a record in a transaction, rather than the instance which was saved last.
#++
# Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false
###
# Configure the query log tags to use the SQLCommenter format.
#++
# Rails.application.config.active_record.query_log_tags_format = :sqlcommenter
###
# Specify the default serializer used by `ActiveRecord::Base#serialize`.
#
# `nil` means there is no default, so every `serialize` call must declare its
# `coder:` explicitly. This app already does that, so this option is safe to
# enable. Do not remove those explicit coders.
#++
# Rails.application.config.active_record.default_column_serializer = nil
###
# Use a 7.1-compatible marshalling format when caching Active Record objects.
#
# This format is more efficient, but cannot be read by Rails 7.0 and earlier, so
# do not enable this until every process is running 7.1 or later.
#++
# Rails.application.config.active_record.marshalling_format_version = 7.1
###
# Generate a `has_secure_token` value when the record is initialized rather than
# when it is created.
#++
# Rails.application.config.active_record.generate_secure_token_on = :initialize
###
# Use a modern HTML5 sanitizer (via Nokogiri's HTML5 parser) rather than the
# legacy HTML4 one, where it is supported.
#
# This changes how malformed markup is parsed and sanitized. Review anywhere the
# application sanitizes user-supplied HTML before enabling it.
#++
# Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor
###
# Parse HTML in tests with an HTML5 parser rather than an HTML4 one.
#++
# Rails.application.config.dom_testing_default_html_version = :html5
###
# Precompile the filter parameters (`config.filter_parameters`) so that filtering
# is faster at request time.
#++
# Rails.application.config.precompile_filter_parameters = true
###
# Log exceptions that are handled by `rescue_from` (or otherwise rendered as an
# error page) at the `error` level rather than `fatal`.
#++
# Rails.application.config.action_dispatch.debug_exception_log_level = :error
###
# Change the cache entry format, so that new entries are written in a format
# which Rails 7.0 and earlier cannot read.
#
# Only enable this once every process is running 7.1 or later, and you have no
# plans to roll back.
#++
# Rails.application.config.active_support.cache_format_version = 7.1
###
# Use SHA-256 to derive Active Record Encryption keys and stop supporting SHA-1
# for non-deterministic encryption.
#
# This app does not currently use Active Record Encryption. If that changes,
# read the upgrade guide before enabling these.
#++
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
# Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = false
@@ -1,72 +0,0 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
#
# This file eases your Rails 7.2 framework defaults upgrade.
#
# Uncomment each configuration one by one to switch to the new default.
# Once your application is ready to run with all new defaults, you can remove
# this file and set the `config.load_defaults` to `7.2`.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
###
# Controls whether Active Job's `#perform_later` and similar methods automatically defer
# the job queuing to after the current Active Record transaction is committed.
#
# Example:
# Topic.transaction do
# topic = Topic.create(...)
# NewTopicNotificationJob.perform_later(topic)
# end
#
# In this example, if the configuration is set to `:never`, the job will
# be enqueued immediately, even though the `Topic` hasn't been committed yet.
# Because of this, if the job is picked up almost immediately, or if the
# transaction doesn't succeed for some reason, the job will fail to find this
# topic in the database.
#
# If `enqueue_after_transaction_commit` is set to `:default`, the queue adapter
# will define the behaviour.
#
# Note: Active Job backends can disable this feature. This is generally done by
# backends that use the same database as Active Record as a queue, hence they
# don't need this feature.
#++
# Rails.application.config.active_job.enqueue_after_transaction_commit = :default
###
# Adds image/webp to the list of content types Active Storage considers as an image
# Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, as they support gif, jpeg, and png.
# This is possible due to broad browser support for WebP, but older browsers and email clients may still not support
# WebP. Requires imagemagick/libvips built with WebP support.
#++
# Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp]
###
# Enable validation of migration timestamps. When set, an ActiveRecord::InvalidMigrationTimestampError
# will be raised if the timestamp prefix for a migration is more than a day ahead of the timestamp
# associated with the current time. This is done to prevent forward-dating of migration files, which can
# impact migration generation and other migration commands.
#
# Applications with existing timestamped migrations that do not adhere to the
# expected format can disable validation by setting this config to `false`.
#++
# Rails.application.config.active_record.validate_migration_timestamps = true
###
# Controls whether the PostgresqlAdapter should decode dates automatically with manual queries.
#
# Example:
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date") #=> Date
#
# This query used to return a `String`.
#++
# Rails.application.config.active_record.postgresql_adapter_decode_dates = true
###
# Enables YJIT as of Ruby 3.3, to bring sizeable performance improvements. If you are
# deploying to a memory constrained environment you may want to set this to `false`.
#++
# Rails.application.config.yjit = true
+4
View File
@@ -12,12 +12,16 @@ Rails.application.routes.draw do
match :verify, on: :member, via: [:get, :post]
get :setup, on: :member
post :check, on: :member
post :regenerate_dkim, on: :member
post :cancel_dkim_regeneration, on: :member
end
resources :servers, except: [:index] do
resources :domains, only: [:index, :new, :create, :destroy] do
match :verify, on: :member, via: [:get, :post]
get :setup, on: :member
post :check, on: :member
post :regenerate_dkim, on: :member
post :cancel_dkim_regeneration, on: :member
end
resources :track_domains do
post :toggle_ssl, on: :member
@@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddPendingDKIMFieldsToDomains < ActiveRecord::Migration[7.0]
def change
add_column :domains, :pending_dkim_private_key, :text
add_column :domains, :pending_dkim_identifier_string, :string
end
end
+3 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_03_11_205229) do
ActiveRecord::Schema[7.1].define(version: 2026_09_03_100000) do
create_table "additional_route_endpoints", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.integer "route_id"
t.string "endpoint_type"
@@ -99,6 +99,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_03_11_205229) do
t.integer "owner_id"
t.string "dkim_identifier_string"
t.boolean "use_for_any"
t.text "pending_dkim_private_key"
t.string "pending_dkim_identifier_string"
t.index ["server_id"], name: "index_domains_on_server_id"
t.index ["uuid"], name: "index_domains_on_uuid", length: 8
end
+1
View File
@@ -68,6 +68,7 @@ This document contains all the environment variables which are available for thi
| `DNS_TRACK_DOMAIN` | String | The CNAME which tracking domains should be pointed to | track.postal.example.com |
| `DNS_HELO_HOSTNAME` | String | The hostname to use in HELO/EHLO when connecting to external SMTP servers | |
| `DNS_DKIM_IDENTIFIER` | String | The identifier to use for DKIM keys in DNS records | postal |
| `DNS_DKIM_KEY_SIZE` | Integer | The size (in bits) of RSA key to generate for DKIM signing (one of 1024, 2048, 3072 or 4096). Note that records for 2048-bit and larger keys exceed 255 characters and must be published as a split (multi-string) TXT record. | 2048 |
| `DNS_DOMAIN_VERIFY_PREFIX` | String | The prefix to add before TXT record verification string | postal-verification |
| `DNS_CUSTOM_RETURN_PATH_PREFIX` | String | The domain to use on external domains which points to the Postal return path domain | psrp |
| `DNS_TIMEOUT` | Integer | The timeout to wait for DNS resolution | 5 |
+2
View File
@@ -147,6 +147,8 @@ dns:
helo_hostname:
# The identifier to use for DKIM keys in DNS records
dkim_identifier: postal
# The size (in bits) of RSA key to generate for DKIM signing (one of 1024, 2048, 3072 or 4096). Note that records for 2048-bit and larger keys exceed 255 characters and must be published as a split (multi-string) TXT record.
dkim_key_size: 2048
# The prefix to add before TXT record verification string
domain_verify_prefix: postal-verification
# The domain to use on external domains which points to the Postal return path domain
+1 -7
View File
@@ -15,7 +15,7 @@ class MigrationWaiter
def wait
attempts_remaining = ATTEMPTS
loop do
pending_migrations = pending_migration_count
pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations.size
if pending_migrations.zero?
Postal.logger.info "no pending migrations, continuing"
return
@@ -33,12 +33,6 @@ class MigrationWaiter
end
end
# Returns the number of migrations which exist in the application but have
# not yet been applied to the database.
def pending_migration_count
ActiveRecord::Base.connection_pool.migration_context.open.pending_migrations.size
end
def wait_if_appropriate
# Don't wait if not configured
return unless Postal::Config.migration_waiter.enabled?
+14
View File
@@ -365,6 +365,20 @@ module Postal
default "postal"
end
integer :dkim_key_size do
description "The size (in bits) of RSA key to generate for DKIM signing (one of 1024, 2048, 3072 or 4096). " \
"Note that records for 2048-bit and larger keys exceed 255 characters and must be " \
"published as a split (multi-string) TXT record."
default 2048
transform do |value|
unless value.nil? || [1024, 2048, 3072, 4096].include?(value)
raise Konfig::Error, "dns.dkim_key_size must be one of 1024, 2048, 3072 or 4096 (got #{value})"
end
value
end
end
string :domain_verify_prefix do
description "The prefix to add before TXT record verification string"
default "postal-verification"
+27 -25
View File
@@ -4,31 +4,33 @@
#
# Table name: domains
#
# id :integer not null, primary key
# server_id :integer
# uuid :string(255)
# name :string(255)
# verification_token :string(255)
# verification_method :string(255)
# verified_at :datetime
# dkim_private_key :text(65535)
# created_at :datetime
# updated_at :datetime
# dns_checked_at :datetime
# spf_status :string(255)
# spf_error :string(255)
# dkim_status :string(255)
# dkim_error :string(255)
# mx_status :string(255)
# mx_error :string(255)
# return_path_status :string(255)
# return_path_error :string(255)
# outgoing :boolean default(TRUE)
# incoming :boolean default(TRUE)
# owner_type :string(255)
# owner_id :integer
# dkim_identifier_string :string(255)
# use_for_any :boolean
# id :integer not null, primary key
# dkim_error :string(255)
# dkim_identifier_string :string(255)
# dkim_private_key :text(65535)
# dkim_status :string(255)
# dns_checked_at :datetime
# incoming :boolean default(TRUE)
# mx_error :string(255)
# mx_status :string(255)
# name :string(255)
# outgoing :boolean default(TRUE)
# owner_type :string(255)
# pending_dkim_identifier_string :string(255)
# pending_dkim_private_key :text(65535)
# return_path_error :string(255)
# return_path_status :string(255)
# spf_error :string(255)
# spf_status :string(255)
# use_for_any :boolean
# uuid :string(255)
# verification_method :string(255)
# verification_token :string(255)
# verified_at :datetime
# created_at :datetime
# updated_at :datetime
# owner_id :integer
# server_id :integer
#
# Indexes
#
@@ -1,60 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe ErrorMessagesHelper, type: :helper do
describe "#error_messages_for" do
let(:user) { User.new }
context "when the record has no errors" do
it "renders nothing" do
expect(helper.error_messages_for(user)).to eq ""
end
end
context "when the record is nil" do
it "renders nothing" do
expect(helper.error_messages_for(nil)).to eq ""
end
end
context "when the record has errors" do
before do
user.valid?
end
it "renders the errorExplanation container the stylesheet targets" do
html = helper.error_messages_for(user)
expect(html).to include %(<div id="errorExplanation" class="errorExplanation">)
end
it "renders each full message as a list item" do
html = helper.error_messages_for(user)
user.errors.full_messages.each do |message|
expect(html).to include "<li>#{ERB::Util.html_escape(message)}</li>"
end
end
it "renders one list item per error" do
html = helper.error_messages_for(user)
expect(html.scan("<li>").size).to eq user.errors.count
end
it "returns an html_safe string" do
expect(helper.error_messages_for(user)).to be_html_safe
end
end
context "when an error message contains HTML" do
before do
user.errors.add(:base, %q(x'"><script>alert(1)</script>))
end
it "escapes it rather than emitting it verbatim" do
html = helper.error_messages_for(user)
expect(html).to_not include "<script>alert(1)</script>"
expect(html).to include "&lt;script&gt;alert(1)&lt;/script&gt;"
end
end
end
end
-34
View File
@@ -1,34 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
# Active Support 7.2 calls `JSON.generate(..., quirks_mode: true)` and
# `JSON.parse(json, quirks_mode: true)`. The json gem removed `quirks_mode` in
# 3.0, so an unpinned json resolves to 3.x and every `to_json` in the app raises
# ArgumentError - webhook payloads and JSON API responses included. The Gemfile
# pins json to < 3 because of this; these examples fail loudly if that pin is
# removed before we are on a Rails version that no longer passes the option.
RSpec.describe "JSON encoding" do
it "encodes a hash" do
expect({ "a" => 1 }.to_json).to eq '{"a":1}'
end
it "encodes nested structures with mixed types" do
payload = { "event" => "MessageSent", "timestamp" => 1.5, "payload" => { "list" => [1, "two", nil, true] } }
expect(JSON.parse(payload.to_json)).to eq payload
end
it "round trips through Active Support's encoder" do
payload = { "uuid" => "abc-123", "nested" => { "x" => [1, 2] } }
expect(ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(payload))).to eq payload
end
it "encodes a bare value, which is what quirks_mode allowed" do
expect(ActiveSupport::JSON.encode("hello")).to eq '"hello"'
expect(ActiveSupport::JSON.encode(42)).to eq "42"
end
it "encodes times the way the webhook payload relies on" do
expect(Time.at(1_700_000_000).utc.to_f.to_json).to eq "1700000000.0"
end
end
-89
View File
@@ -1,89 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
require "migration_waiter"
RSpec.describe MigrationWaiter do
describe ".pending_migration_count" do
# This talks to Active Record's migration APIs directly and is the part of
# this class most likely to break on a Rails upgrade (the call moved from
# the connection to the connection pool in Rails 7.2). The rest of this
# class is stubbed, so this example is what actually exercises it.
it "returns the number of migrations which have not been applied" do
expect(described_class.pending_migration_count).to be_a Integer
end
it "returns zero when the test database is up to date" do
expect(described_class.pending_migration_count).to eq 0
end
end
describe ".wait" do
before do
allow(Postal.logger).to receive(:info)
allow(described_class).to receive(:sleep)
end
it "returns immediately when there are no pending migrations" do
allow(described_class).to receive(:pending_migration_count).and_return(0)
described_class.wait
expect(described_class).to have_received(:pending_migration_count).once
expect(described_class).to_not have_received(:sleep)
end
it "waits until there are no pending migrations remaining" do
allow(described_class).to receive(:pending_migration_count).and_return(2, 1, 0)
described_class.wait
expect(described_class).to have_received(:sleep).with(described_class::SLEEP_TIME).twice
end
it "exits when migrations are still pending after the maximum number of attempts" do
stub_const("#{described_class}::ATTEMPTS", 3)
allow(described_class).to receive(:pending_migration_count).and_return(1)
# Process.exit raises SystemExit for real. It has to do so here too,
# otherwise the wait loop never terminates.
allow(Process).to receive(:exit).and_raise(SystemExit)
expect { described_class.wait }.to raise_error(SystemExit)
expect(Process).to have_received(:exit).with(1)
expect(described_class).to have_received(:sleep).twice
end
end
describe ".wait_if_appropriate" do
before do
allow(described_class).to receive(:wait)
allow(described_class).to receive(:console?).and_return(false)
allow(described_class).to receive(:rake_task?).and_return(false)
allow(described_class).to receive(:rails_command?).and_return(false)
end
context "when the migration waiter is not enabled" do
it "does not wait" do
allow(Postal::Config.migration_waiter).to receive(:enabled?).and_return(false)
described_class.wait_if_appropriate
expect(described_class).to_not have_received(:wait)
end
end
context "when the migration waiter is enabled" do
before do
allow(Postal::Config.migration_waiter).to receive(:enabled?).and_return(true)
end
it "waits" do
described_class.wait_if_appropriate
expect(described_class).to have_received(:wait)
end
%w[console? rake_task? rails_command?].each do |predicate|
it "does not wait when #{predicate.delete_suffix('?')} is true" do
allow(described_class).to receive(predicate).and_return(true)
described_class.wait_if_appropriate
expect(described_class).to_not have_received(:wait)
end
end
end
end
end
+300 -25
View File
@@ -4,31 +4,33 @@
#
# Table name: domains
#
# id :integer not null, primary key
# dkim_error :string(255)
# dkim_identifier_string :string(255)
# dkim_private_key :text(65535)
# dkim_status :string(255)
# dns_checked_at :datetime
# incoming :boolean default(TRUE)
# mx_error :string(255)
# mx_status :string(255)
# name :string(255)
# outgoing :boolean default(TRUE)
# owner_type :string(255)
# return_path_error :string(255)
# return_path_status :string(255)
# spf_error :string(255)
# spf_status :string(255)
# use_for_any :boolean
# uuid :string(255)
# verification_method :string(255)
# verification_token :string(255)
# verified_at :datetime
# created_at :datetime
# updated_at :datetime
# owner_id :integer
# server_id :integer
# id :integer not null, primary key
# dkim_error :string(255)
# dkim_identifier_string :string(255)
# dkim_private_key :text(65535)
# dkim_status :string(255)
# dns_checked_at :datetime
# incoming :boolean default(TRUE)
# mx_error :string(255)
# mx_status :string(255)
# name :string(255)
# outgoing :boolean default(TRUE)
# owner_type :string(255)
# pending_dkim_identifier_string :string(255)
# pending_dkim_private_key :text(65535)
# return_path_error :string(255)
# return_path_status :string(255)
# spf_error :string(255)
# spf_status :string(255)
# use_for_any :boolean
# uuid :string(255)
# verification_method :string(255)
# verification_token :string(255)
# verified_at :datetime
# created_at :datetime
# updated_at :datetime
# owner_id :integer
# server_id :integer
#
# Indexes
#
@@ -162,6 +164,22 @@ describe Domain do
it "generates a new dkim key" do
expect { domain.generate_dkim_key }.to change { domain.dkim_private_key }.from(nil).to(match(/\A-+BEGIN RSA PRIVATE KEY-+/))
end
it "generates a key of the configured size" do
domain.generate_dkim_key
expect(OpenSSL::PKey::RSA.new(domain.dkim_private_key).n.num_bits).to eq Postal::Config.dns.dkim_key_size
end
context "when a different key size is configured" do
before do
allow(Postal::Config.dns).to receive(:dkim_key_size).and_return(1024)
end
it "generates a key of that size" do
domain.generate_dkim_key
expect(OpenSSL::PKey::RSA.new(domain.dkim_private_key).n.num_bits).to eq 1024
end
end
end
describe "#dkim_key" do
@@ -183,6 +201,138 @@ describe Domain do
end
end
describe "#pending_dkim_key" do
context "when the domain has a pending DKIM key" do
let(:domain) { create(:domain, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "returns the pending dkim key as a OpenSSL::PKey::RSA" do
expect(domain.pending_dkim_key).to be_a OpenSSL::PKey::RSA
expect(domain.pending_dkim_key.to_s).to eq domain.pending_dkim_private_key
end
end
context "when the domain has no pending DKIM key" do
let(:domain) { create(:domain) }
it "returns nil" do
expect(domain.pending_dkim_key).to be_nil
end
end
end
describe "#regenerate_dkim_key!" do
context "when the current DKIM record is verified" do
let(:domain) { create(:domain, dkim_status: "OK") }
it "stores the new key as a pending key with a new identifier" do
domain.regenerate_dkim_key!
expect(domain.pending_dkim_private_key).to match(/\A-+BEGIN RSA PRIVATE KEY-+/)
expect(domain.pending_dkim_identifier_string).to be_present
expect(domain.pending_dkim_identifier_string).to_not eq domain.dkim_identifier_string
end
it "does not change the active key or status" do
expect { domain.regenerate_dkim_key! }.to_not(change { domain.reload.dkim_private_key })
expect(domain.dkim_status).to eq "OK"
end
end
context "when the current DKIM record is not verified" do
let(:domain) { create(:domain, dkim_status: "Missing", dkim_error: "No TXT records") }
it "replaces the active key immediately without creating a pending key" do
original_key = domain.dkim_key.to_s
domain.regenerate_dkim_key!
expect(domain.reload.dkim_private_key).to_not eq original_key
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.dkim_key.to_s).to eq domain.dkim_private_key
end
it "resets the DKIM status" do
domain.regenerate_dkim_key!
expect(domain.dkim_status).to be_nil
expect(domain.dkim_error).to be_nil
end
context "when a key change was already in progress" do
# This happens when the active record breaks (so the status is no longer
# "OK") while a pending key is waiting to be activated. The immediate
# replacement supersedes the in-flight change, so the stale pending key
# must not be left behind to be activated later.
let(:domain) { create(:domain, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
domain.update!(dkim_status: "Missing", dkim_error: "No TXT records")
end
it "abandons the pending key" do
expect(domain.pending_dkim_private_key).to_not be_nil
domain.regenerate_dkim_key!
expect(domain.reload.pending_dkim_private_key).to be_nil
expect(domain.pending_dkim_identifier_string).to be_nil
expect(domain.pending_dkim_key).to be_nil
end
it "replaces the active key rather than promoting the pending one" do
stale_pending_key = domain.pending_dkim_private_key
original_key = domain.dkim_private_key
domain.regenerate_dkim_key!
expect(domain.reload.dkim_private_key).to_not eq original_key
expect(domain.dkim_private_key).to_not eq stale_pending_key
end
end
end
end
describe "#activate_pending_dkim_key" do
context "when there is no pending key" do
let(:domain) { create(:domain) }
it "does nothing" do
expect { domain.activate_pending_dkim_key }.to_not(change { domain.dkim_private_key })
end
end
context "when there is a pending key" do
let(:domain) { create(:domain, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "promotes the pending key and identifier and clears the pending values" do
pending_key = domain.pending_dkim_private_key
pending_identifier = domain.pending_dkim_identifier_string
domain.activate_pending_dkim_key
expect(domain.dkim_private_key).to eq pending_key
expect(domain.dkim_identifier_string).to eq pending_identifier
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.pending_dkim_identifier_string).to be_nil
expect(domain.dkim_key.to_s).to eq pending_key
expect(domain.pending_dkim_key).to be_nil
end
end
end
describe "#cancel_pending_dkim_key!" do
let(:domain) { create(:domain, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "discards the pending key without changing the active key" do
expect { domain.cancel_pending_dkim_key! }.to_not(change { domain.reload.dkim_private_key })
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.pending_dkim_identifier_string).to be_nil
end
end
describe "#to_param" do
context "when the domain has not been saved" do
it "returns nil" do
@@ -274,6 +424,131 @@ describe Domain do
end
end
describe "#pending_dkim_record" do
context "when the domain has no pending DKIM key" do
it "returns nil" do
expect(domain.pending_dkim_record).to be_nil
end
end
context "when the domain has a pending DKIM key" do
let(:domain) { create(:domain, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "returns the DKIM record for the pending key" do
expect(domain.pending_dkim_record).to match(/\Av=DKIM1; t=s; h=sha256; p=.*;\z/)
expect(domain.pending_dkim_record).to_not eq domain.dkim_record
end
it "returns a record name using the pending identifier" do
expect(domain.pending_dkim_record_name).to eq "#{Postal::Config.dns.dkim_identifier}-#{domain.pending_dkim_identifier_string}._domainkey"
end
end
end
describe "#check_dkim_record" do
let(:domain) { create(:domain, dkim_status: "OK") }
let(:resolver) { instance_double(DNSResolver) }
before do
allow(domain).to receive(:resolver).and_return(resolver)
end
context "when there is a pending DKIM key" do
before do
domain.regenerate_dkim_key!
end
context "when the pending record has been published" do
it "activates and verifies the pending key from a single DNS response" do
responses = [[domain.pending_dkim_record], []]
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}") { responses.shift }
pending_key = domain.pending_dkim_private_key
domain.check_dkim_record
expect(domain.dkim_private_key).to eq pending_key
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.dkim_status).to eq "OK"
expect(domain.dkim_error).to be_nil
expect(responses).to eq([[]])
end
end
context "when multiple pending records have been published" do
before do
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}").and_return([domain.pending_dkim_record, domain.pending_dkim_record])
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([domain.dkim_record])
end
it "keeps the current key active and retains the pending key" do
original_key = domain.dkim_private_key
domain.check_dkim_record
expect(domain.dkim_private_key).to eq original_key
expect(domain.pending_dkim_private_key).to_not be_nil
expect(domain.dkim_status).to eq "OK"
end
end
context "when the pending record has not been published" do
before do
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}").and_return([])
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([domain.dkim_record])
end
it "keeps the current key active and retains the pending key" do
original_key = domain.dkim_private_key
domain.check_dkim_record
expect(domain.dkim_private_key).to eq original_key
expect(domain.pending_dkim_private_key).to_not be_nil
expect(domain.dkim_status).to eq "OK"
end
end
context "when the pending record does not match" do
before do
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}").and_return(["v=DKIM1; t=s; h=sha256; p=something;"])
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([domain.dkim_record])
end
it "keeps the current key active and retains the pending key" do
original_key = domain.dkim_private_key
domain.check_dkim_record
expect(domain.dkim_private_key).to eq original_key
expect(domain.pending_dkim_private_key).to_not be_nil
expect(domain.dkim_status).to eq "OK"
end
end
end
context "when there is no pending DKIM key" do
context "when the record is published" do
before do
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([domain.dkim_record])
end
it "marks the record as OK" do
domain.check_dkim_record
expect(domain.dkim_status).to eq "OK"
expect(domain.dkim_error).to be_nil
end
end
context "when the record is missing" do
before do
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([])
end
it "marks the record as missing" do
domain.check_dkim_record
expect(domain.dkim_status).to eq "Missing"
end
end
end
end
describe "#return_path_domain" do
it "returns the return path domain" do
expect(domain.return_path_domain).to eq "#{Postal::Config.dns.custom_return_path_prefix}.#{domain.name}"
-68
View File
@@ -1,68 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
# Active Record's serialization defaults have changed across recent Rails
# releases (`config.active_record.default_column_serializer` becomes `nil` in
# the 7.1 defaults, which makes an implicit coder an error rather than YAML).
# These columns hold data written by older versions of Postal, so the on-disk
# format has to stay YAML. These examples pin both the format and the round trip
# so a future framework defaults bump cannot change them silently.
RSpec.describe "serialized columns" do
shared_examples "a YAML serialized hash column" do |attribute|
let(:value) { { "string" => "value", "number" => 123, "nested" => { "a" => [1, 2] } } }
def write_raw(record, attribute, raw)
sql = described_class.sanitize_sql_array(
["UPDATE #{described_class.table_name} SET #{attribute} = ? WHERE id = ?", raw, record.id]
)
described_class.connection.update(sql)
end
def read_raw(record, attribute)
described_class.connection.select_value(
described_class.sanitize_sql_array(
["SELECT #{attribute} FROM #{described_class.table_name} WHERE id = ?", record.id]
)
)
end
it "defaults to an empty hash" do
expect(described_class.new.public_send(attribute)).to eq({})
end
it "round trips a hash through the database" do
record.update!(attribute => value)
expect(record.reload.public_send(attribute)).to eq value
end
it "stores the value as YAML in the database" do
record.update!(attribute => value)
raw = read_raw(record, attribute)
expect(raw).to be_a String
expect(YAML.safe_load(raw)).to eq value
end
it "reads YAML which was written by a previous version of Postal" do
write_raw(record, attribute, value.to_yaml)
expect(record.reload.public_send(attribute)).to eq value
end
it "rejects a value which is not a hash" do
expect { record.update!(attribute => "not a hash") }.to raise_error ActiveRecord::SerializationTypeMismatch
end
end
describe Credential do
let(:record) { create(:credential) }
include_examples "a YAML serialized hash column", :options
end
describe WebhookRequest do
let(:record) { create(:webhook_request) }
include_examples "a YAML serialized hash column", :payload
end
end
+144
View File
@@ -0,0 +1,144 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "DomainsController", type: :request do
let(:user) { create(:user, admin: true) }
let(:organization) { create(:organization, owner: user) }
let(:server) { create(:server, organization: organization) }
before do
post "/login", params: { email_address: user.email_address, password: "passw0rd" }
end
describe "POST /org/:org/servers/:server/domains/:domain/regenerate_dkim" do
context "when the domain's DKIM record is verified" do
let(:domain) { create(:domain, owner: server, dkim_status: "OK") }
it "generates a pending DKIM key without changing the active key" do
original_key = domain.dkim_private_key
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/regenerate_dkim"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
expect(flash[:notice]).to include("Keep the old DNS record published afterward")
expect(flash[:notice]).to include("queued, held, or available for redelivery")
expect(flash[:notice]).to_not include("you can remove the old record")
domain.reload
expect(domain.dkim_private_key).to eq original_key
expect(domain.pending_dkim_private_key).to_not be_nil
end
end
context "when the domain's DKIM record is not verified" do
let(:domain) { create(:domain, owner: server, dkim_status: "Missing") }
it "replaces the active key immediately" do
original_key = domain.dkim_private_key
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/regenerate_dkim"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
domain.reload
expect(domain.dkim_private_key).to_not eq original_key
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.dkim_status).to be_nil
end
end
context "when a key change is already in progress" do
let(:domain) { create(:domain, owner: server, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "refuses and leaves the pending key untouched" do
existing_pending_key = domain.pending_dkim_private_key
existing_pending_identifier = domain.pending_dkim_identifier_string
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/regenerate_dkim"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
expect(flash[:alert]).to include("already in progress")
domain.reload
expect(domain.pending_dkim_private_key).to eq existing_pending_key
expect(domain.pending_dkim_identifier_string).to eq existing_pending_identifier
end
it "does not change the active key" do
expect do
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/regenerate_dkim"
end.to_not(change { domain.reload.dkim_private_key })
end
end
end
describe "POST /org/:org/domains/:domain/regenerate_dkim" do
let(:domain) { create(:domain, owner: organization, dkim_status: "OK") }
it "generates a pending DKIM key for an organization-owned domain" do
post "/org/#{organization.permalink}/domains/#{domain.uuid}/regenerate_dkim"
expect(response).to redirect_to(setup_organization_domain_path(organization, domain))
expect(domain.reload.pending_dkim_private_key).to_not be_nil
end
end
describe "POST /org/:org/servers/:server/domains/:domain/check with a pending DKIM key" do
let(:domain) { create(:domain, owner: server, dkim_status: "OK") }
let(:resolver) { instance_double(DNSResolver) }
before do
domain.regenerate_dkim_key!
allow(DNSResolver).to receive(:for_domain).and_return(resolver)
allow(resolver).to receive(:txt).with(domain.name).and_return([domain.spf_record])
allow(resolver).to receive(:mx).with(domain.name).and_return([])
allow(resolver).to receive(:cname).and_return([])
end
context "when the pending DKIM record has been published" do
before do
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}").and_return([domain.pending_dkim_record])
end
it "activates the pending key and tells the user to retain the old record" do
old_record_name = domain.dkim_record_name
new_key = domain.pending_dkim_private_key
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/check"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
expect(flash[:notice]).to include("new DKIM key is now active")
expect(flash[:notice]).to include(old_record_name)
expect(flash[:notice]).to include("queued, held, or available for redelivery")
expect(flash[:notice]).to_not include("can remove")
domain.reload
expect(domain.dkim_private_key).to eq new_key
expect(domain.pending_dkim_private_key).to be_nil
expect(domain.dkim_status).to eq "OK"
end
end
context "when the pending DKIM record has not been published" do
before do
allow(resolver).to receive(:txt).with("#{domain.pending_dkim_record_name}.#{domain.name}").and_return([])
allow(resolver).to receive(:txt).with("#{domain.dkim_record_name}.#{domain.name}").and_return([domain.dkim_record])
end
it "keeps the current key active and tells the user the new record wasn't verified" do
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/check"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
expect(flash[:alert]).to include("couldn't verify the record for your new DKIM key")
domain.reload
expect(domain.pending_dkim_private_key).to_not be_nil
expect(domain.dkim_status).to eq "OK"
end
end
end
describe "POST /org/:org/servers/:server/domains/:domain/cancel_dkim_regeneration" do
let(:domain) { create(:domain, owner: server, dkim_status: "OK") }
before do
domain.regenerate_dkim_key!
end
it "discards the pending DKIM key" do
post "/org/#{organization.permalink}/servers/#{server.permalink}/domains/#{domain.uuid}/cancel_dkim_regeneration"
expect(response).to redirect_to(setup_organization_server_domain_path(organization, server, domain))
expect(domain.reload.pending_dkim_private_key).to be_nil
end
end
end
-90
View File
@@ -1,90 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
# These examples exist to exercise the view layer end to end - HAML, the asset
# pipeline, the layout, form helpers and the session middleware. The rest of the
# request specs only assert on redirects, so without these nothing in the suite
# would notice if a Rails upgrade broke rendering.
RSpec.describe "rendering", type: :request do
before do
host! Postal::Config.postal.web_hostname
end
describe "the login page" do
it "renders" do
get "/login"
expect(response).to have_http_status(:ok)
expect(response.media_type).to eq "text/html"
expect(response.body).to include "<form"
expect(response.body).to include "password"
end
it "renders the layout including compiled assets" do
get "/login"
expect(response.body).to include "<html"
expect(response.body).to match(/<link[^>]+stylesheets?/i).or match(/<link[^>]+\.css/i)
expect(response.body).to match(/<script[^>]+\.js/i)
end
end
describe "when not authenticated" do
it "redirects to the login page" do
get "/"
expect(response).to redirect_to("/login?return_to=%2F")
end
end
describe "when authenticated" do
let(:user) { create(:user) }
before do
post "/login", params: { email_address: user.email_address, password: "passw0rd" }
end
it "renders the organizations index" do
get "/"
expect(response).to have_http_status(:ok)
expect(response.body).to include "<html"
end
it "renders the user settings page" do
get "/settings"
expect(response).to have_http_status(:ok)
expect(response.body).to include user.email_address
end
# `f.error_messages` and `error_messages_for` used to come from the
# dynamic_form gem. The app provides them itself now (see
# ErrorMessagesHelper and config/initializers/form_builder_extensions.rb),
# so the rendered output is our responsibility to keep working.
describe "a form which fails validation" do
before do
patch "/settings", params: {
password: "passw0rd",
user: { first_name: "", last_name: "Smith", email_address: user.email_address }
}
end
it "re-renders the form" do
expect(response).to have_http_status(:ok)
end
it "renders the validation errors in the errorExplanation container" do
expect(response.body).to include %(<div id="errorExplanation" class="errorExplanation">)
expect(response.body).to include "<li>First name can&#39;t be blank</li>"
end
end
it "does not render an error container on a form which has no errors" do
get "/settings"
expect(response.body).to_not include "errorExplanation"
end
end
end
-86
View File
@@ -1,86 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
# The security headers come from the secure_headers gem plus Rails' own
# defaults, and both have changed them across major versions. Nothing else in
# the suite looks at response headers, so pin them here.
RSpec.describe "security headers", type: :request do
before do
host! Postal::Config.postal.web_hostname
get "/login"
end
it "responds successfully" do
expect(response).to have_http_status(:ok)
end
it "sets X-Frame-Options to sameorigin" do
expect(response.headers["x-frame-options"]).to eq "sameorigin"
end
it "sets X-Content-Type-Options to nosniff" do
expect(response.headers["x-content-type-options"]).to eq "nosniff"
end
it "disables the legacy XSS auditor rather than enabling it" do
# secure_headers 7 changed this from "1; mode=block". `0` is correct: the
# legacy auditor is deprecated and blocking mode was itself exploitable.
expect(response.headers["x-xss-protection"]).to eq "0"
end
it "sets X-Permitted-Cross-Domain-Policies to none" do
expect(response.headers["x-permitted-cross-domain-policies"]).to eq "none"
end
describe "the content security policy" do
let(:csp) { response.headers["content-security-policy"] }
it "is present" do
expect(csp).to be_present
end
it "restricts script-src to self" do
expect(csp).to include "script-src 'self'"
end
it "restricts connect-src to self" do
expect(csp).to include "connect-src 'self'"
end
it "forbids object-src" do
expect(csp).to include "object-src 'none'"
end
end
describe "cookies" do
# Rack 3 returns set-cookie as a String for one cookie and an Array for
# several, so normalise before looking for a particular cookie.
def cookie_named(name)
Array(response.headers["set-cookie"]).flat_map { |v| v.split("\n") }.find { |c| c.start_with?("#{name}=") }
end
it "marks the browser id cookie httponly and samesite lax" do
expect(cookie_named("browser_id")).to include "httponly"
expect(cookie_named("browser_id")).to include "samesite=lax"
end
context "once logged in" do
let(:user) { create(:user) }
before do
post "/login", params: { email_address: user.email_address, password: "passw0rd" }
end
it "marks the authie session cookie httponly and samesite lax" do
expect(cookie_named("user_session")).to include "httponly"
expect(cookie_named("user_session")).to include "samesite=lax"
end
it "marks the rails session cookie httponly and samesite lax" do
expect(cookie_named("_postal_session")).to include "httponly"
expect(cookie_named("_postal_session")).to include "samesite=lax"
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "domains/index", type: :view do
let(:user) { create(:user, admin: true) }
let(:organization) { create(:organization, owner: user) }
let(:domain) { create(:domain, owner: organization, dkim_status: "OK") }
before do
assign(:organization, organization)
assign(:domains, [domain])
assign(:server, nil)
without_partial_double_verification do
allow(view).to receive(:organization).and_return(organization)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:page_title).and_return(["Postal"])
end
end
context "when no key change is in progress" do
it "does not flag a pending DKIM key" do
render
expect(rendered).to_not include("New DKIM key pending")
end
end
context "when a key change is in progress" do
before do
domain.regenerate_dkim_key!
end
it "flags the domain as having a pending DKIM key" do
render
expect(rendered).to include("New DKIM key pending")
end
it "links the flag to the DNS setup page" do
render
expect(rendered).to include(setup_organization_domain_path(organization, domain))
end
it "still shows DKIM as OK because the active key is unaffected" do
render
expect(rendered).to include("domainList__check--ok")
end
end
end
@@ -0,0 +1,73 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "domains/setup", type: :view do
let(:user) { create(:user, admin: true) }
let(:organization) { create(:organization, owner: user) }
let(:domain) { create(:domain, owner: organization, dkim_status: "OK") }
before do
assign(:organization, organization)
assign(:domain, domain)
assign(:server, nil)
without_partial_double_verification do
allow(view).to receive(:organization).and_return(organization)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:page_title).and_return(["Postal"])
end
end
context "when no key change is in progress" do
it "shows the active record and offers regeneration" do
render
expect(rendered).to include(domain.dkim_record_name)
expect(rendered).to include(domain.dkim_record)
expect(rendered).to include("Regenerate DKIM key")
expect(rendered).to_not include("Key change in progress")
expect(rendered).to_not include("Cancel new DKIM key")
end
end
context "when a key change is in progress" do
before do
domain.regenerate_dkim_key!
end
it "shows both the current and the new record" do
render
expect(rendered).to include(domain.dkim_record_name)
expect(rendered).to include(domain.dkim_record)
expect(rendered).to include(domain.pending_dkim_record_name)
expect(rendered).to include(domain.pending_dkim_record)
end
it "labels which record is active and which must be added" do
render
expect(rendered).to include("Key change in progress")
expect(rendered).to include("Keep this record exactly as it is")
expect(rendered).to include("Add this record as well")
end
it "tells the user to add the new record rather than replace the old one" do
render
expect(rendered).to include("alongside the record above rather than replacing it")
end
it "does not describe the active record as one which needs adding" do
render
expect(rendered).to_not include("You need to add a new TXT record")
end
it "offers cancellation instead of another regeneration" do
render
expect(rendered).to include("Cancel new DKIM key")
expect(rendered).to_not include("Regenerate DKIM key")
end
it "does not claim the DKIM record is fully good" do
render
expect(rendered).to_not include("Your DKIM record looks good!")
end
end
end
-46
View File
@@ -1,46 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
require "haml"
# Compiles every HAML template and checks the Ruby it produces actually parses.
# A HAML major version bump changes the compiler, and the request specs only
# render a handful of pages, so without this most of app/views is unverified
# until someone visits the page in production.
RSpec.describe "HAML templates" do
def compile(source)
# Wrapped in a method body because layouts use `yield`, which is not legal
# at the top level.
RubyVM::AbstractSyntaxTree.parse("def _template\n#{Haml::Engine.new.call(source)}\nend")
end
templates = Dir[Rails.root.join("app/views/**/*.haml")]
it "finds the templates to check" do
expect(templates.size).to be > 50
end
# Without this, a change in how HAML reports errors could quietly turn every
# example below into a no-op that passes on anything.
describe "the check itself" do
it "rejects a template whose Ruby does not parse" do
expect { compile("= foo(\n") }.to raise_error(SyntaxError)
end
it "rejects a template with a malformed attribute hash" do
expect { compile("%div{:a =>}\n") }.to raise_error(SyntaxError)
end
it "accepts a valid template" do
expect { compile("%div\n %p hello\n") }.to_not raise_error
end
end
templates.each do |path|
relative = path.sub("#{Rails.root}/", "")
it "compiles #{relative}" do
expect { compile(File.read(path)) }.to_not raise_error
end
end
end