Encrypt outgoing emails when possible
This commit is contained in:
parent
3ee76e26ab
commit
c4c2d16342
@ -1,3 +1,89 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
layout 'mailer'
|
||||
|
||||
private
|
||||
|
||||
def send_mail
|
||||
@template ||= "#{self.class.name.underscore}/#{caller[0][/`([^']*)'/, 1]}"
|
||||
headers['Message-ID'] = message_id
|
||||
|
||||
if @user.pgp_pubkey.present?
|
||||
mail(to: @user.email, subject: "...", content_type: pgp_content_type) do |format|
|
||||
format.text { render plain: pgp_content }
|
||||
end
|
||||
else
|
||||
mail(to: @user.email, subject: @subject) do |format|
|
||||
format.text { render @template }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def from_address
|
||||
ENV.fetch('SMTP_FROM_ADDRESS', 'accounts@localhost')
|
||||
end
|
||||
|
||||
def from_domain
|
||||
Mail::Address.new(from_address).domain
|
||||
end
|
||||
|
||||
def message_id
|
||||
@message_id ||= "#{SecureRandom.uuid}@#{from_domain}"
|
||||
end
|
||||
|
||||
def boundary
|
||||
@boundary ||= SecureRandom.hex(8)
|
||||
end
|
||||
|
||||
def pgp_content_type
|
||||
"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"------------#{boundary}\""
|
||||
end
|
||||
|
||||
def pgp_nested_content
|
||||
message_content = render_to_string(template: @template)
|
||||
message_content_base64 = Base64.encode64(message_content)
|
||||
nested_boundary = SecureRandom.hex(8)
|
||||
|
||||
<<~NESTED_CONTENT
|
||||
Content-Type: multipart/mixed; boundary="------------#{nested_boundary}"; protected-headers="v1"
|
||||
Subject: #{@subject}
|
||||
From: <#{from_address}>
|
||||
To: #{@user.display_name || @user.cn} <#{@user.email}>
|
||||
Message-ID: <#{message_id}>
|
||||
|
||||
--------------#{nested_boundary}
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
#{message_content_base64}
|
||||
|
||||
--------------#{nested_boundary}--
|
||||
NESTED_CONTENT
|
||||
end
|
||||
|
||||
def pgp_content
|
||||
encrypted_content = UserManager::PgpEncrypt.call(user: @user, text: pgp_nested_content)
|
||||
encrypted_base64 = Base64.encode64(encrypted_content.to_s)
|
||||
|
||||
<<~EMAIL_CONTENT
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------#{boundary}
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------#{boundary}
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
#{encrypted_base64}
|
||||
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------#{boundary}--
|
||||
EMAIL_CONTENT
|
||||
end
|
||||
end
|
||||
|
@ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer
|
||||
@user = params[:user]
|
||||
@subject = params[:subject]
|
||||
@body = params[:body]
|
||||
mail(to: @user.email, subject: @subject)
|
||||
send_mail
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer
|
||||
@user = params[:user]
|
||||
@amount_sats = params[:amount_sats]
|
||||
@subject = "Sats received"
|
||||
mail to: @user.email, subject: @subject
|
||||
send_mail
|
||||
end
|
||||
|
||||
def remotestorage_auth_created
|
||||
@ -15,19 +15,19 @@ class NotificationMailer < ApplicationMailer
|
||||
"#{access} #{directory}"
|
||||
end
|
||||
@subject = "New app connected to your storage"
|
||||
mail to: @user.email, subject: @subject
|
||||
send_mail
|
||||
end
|
||||
|
||||
def new_invitations_available
|
||||
@user = params[:user]
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
send_mail
|
||||
end
|
||||
|
||||
def bitcoin_donation_confirmed
|
||||
@user = params[:user]
|
||||
@donation = params[:donation]
|
||||
@subject = "Donation confirmed"
|
||||
mail to: @user.email, subject: @subject
|
||||
send_mail
|
||||
end
|
||||
end
|
||||
|
19
app/services/user_manager/pgp_encrypt.rb
Normal file
19
app/services/user_manager/pgp_encrypt.rb
Normal file
@ -0,0 +1,19 @@
|
||||
require 'gpgme'
|
||||
|
||||
module UserManager
|
||||
class PgpEncrypt < UserManagerService
|
||||
def initialize(user:, text:)
|
||||
@user = user
|
||||
@text = text
|
||||
end
|
||||
|
||||
def call
|
||||
crypto = GPGME::Crypto.new
|
||||
crypto.encrypt(
|
||||
@text,
|
||||
recipients: @user.gnupg_key,
|
||||
always_trust: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
87
spec/mailers/notification_mailer_spec.rb
Normal file
87
spec/mailers/notification_mailer_spec.rb
Normal file
@ -0,0 +1,87 @@
|
||||
# spec/mailers/welcome_mailer_spec.rb
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NotificationMailer, type: :mailer do
|
||||
describe '#lightning_sats_received' do
|
||||
|
||||
context "without PGP key" do
|
||||
let(:user) { create(:user, cn: "phil", email: 'phil@example.com') }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: nil
|
||||
})
|
||||
end
|
||||
|
||||
describe "unencrypted email" do
|
||||
let(:mail) { described_class.with(user: user, amount_sats: 21000).lightning_sats_received }
|
||||
|
||||
it 'renders the correct to/from headers' do
|
||||
expect(mail.to).to eq([user.email])
|
||||
expect(mail.from).to eq(['accounts@kosmos.org'])
|
||||
end
|
||||
|
||||
it 'renders the correct subject' do
|
||||
expect(mail.subject).to eq('Sats received')
|
||||
end
|
||||
|
||||
it 'uses the correct content type' do
|
||||
expect(mail.header['content-type'].to_s).to include('text/plain')
|
||||
end
|
||||
|
||||
it 'renders the body with correct content' do
|
||||
expect(mail.body.encoded).to match(/You just received 21,000 sats/)
|
||||
expect(mail.body.encoded).to include(user.address)
|
||||
end
|
||||
|
||||
it 'includes a link to the lightning service page' do
|
||||
expect(mail.body.encoded).to include("https://accounts.kosmos.org/services/lightning")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with PGP key" do
|
||||
let(:pgp_pubkey) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
|
||||
let(:pgp_fingerprint) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
|
||||
let(:user) { create(:user, id: 2, cn: "alice", email: 'alice@example.com', pgp_fpr: pgp_fingerprint) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: pgp_pubkey
|
||||
})
|
||||
end
|
||||
|
||||
describe "encrypted email" do
|
||||
let(:mail) { described_class.with(user: user, amount_sats: 21000).lightning_sats_received }
|
||||
|
||||
it 'renders the correct to/from headers' do
|
||||
expect(mail.to).to eq([user.email])
|
||||
expect(mail.from).to eq(['accounts@kosmos.org'])
|
||||
end
|
||||
|
||||
it 'encrypts the subject line' do
|
||||
expect(mail.subject).to eq('...')
|
||||
end
|
||||
|
||||
it 'uses the correct content type' do
|
||||
expect(mail.header['content-type'].to_s).to include('multipart/encrypted')
|
||||
expect(mail.header['content-type'].to_s).to include('protocol="application/pgp-encrypted"')
|
||||
end
|
||||
|
||||
it 'renders the PGP version part' do
|
||||
expect(mail.body.encoded).to include("Content-Type: application/pgp-encrypted")
|
||||
expect(mail.body.encoded).to include("Content-Description: PGP/MIME version identification")
|
||||
expect(mail.body.encoded).to include("Version: 1")
|
||||
end
|
||||
|
||||
it 'renders the encrypted PGP part' do
|
||||
expect(mail.body.encoded).to include('Content-Type: application/octet-stream; name="encrypted.asc"')
|
||||
expect(mail.body.encoded).to include('Content-Description: OpenPGP encrypted message')
|
||||
expect(mail.body.encoded).to include('Content-Disposition: inline; filename="encrypted.asc"')
|
||||
expect(mail.body.encoded).to include('-----BEGIN PGP MESSAGE-----')
|
||||
expect(mail.body.encoded).to include('hF4DR')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user