Encrypt outgoing emails when possible
This commit is contained in:
parent
3ee76e26ab
commit
c4c2d16342
@ -1,3 +1,89 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
layout 'mailer'
|
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
|
end
|
||||||
|
@ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer
|
|||||||
@user = params[:user]
|
@user = params[:user]
|
||||||
@subject = params[:subject]
|
@subject = params[:subject]
|
||||||
@body = params[:body]
|
@body = params[:body]
|
||||||
mail(to: @user.email, subject: @subject)
|
send_mail
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@user = params[:user]
|
@user = params[:user]
|
||||||
@amount_sats = params[:amount_sats]
|
@amount_sats = params[:amount_sats]
|
||||||
@subject = "Sats received"
|
@subject = "Sats received"
|
||||||
mail to: @user.email, subject: @subject
|
send_mail
|
||||||
end
|
end
|
||||||
|
|
||||||
def remotestorage_auth_created
|
def remotestorage_auth_created
|
||||||
@ -15,19 +15,19 @@ class NotificationMailer < ApplicationMailer
|
|||||||
"#{access} #{directory}"
|
"#{access} #{directory}"
|
||||||
end
|
end
|
||||||
@subject = "New app connected to your storage"
|
@subject = "New app connected to your storage"
|
||||||
mail to: @user.email, subject: @subject
|
send_mail
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_invitations_available
|
def new_invitations_available
|
||||||
@user = params[:user]
|
@user = params[:user]
|
||||||
@subject = "New invitations added to your account"
|
@subject = "New invitations added to your account"
|
||||||
mail to: @user.email, subject: @subject
|
send_mail
|
||||||
end
|
end
|
||||||
|
|
||||||
def bitcoin_donation_confirmed
|
def bitcoin_donation_confirmed
|
||||||
@user = params[:user]
|
@user = params[:user]
|
||||||
@donation = params[:donation]
|
@donation = params[:donation]
|
||||||
@subject = "Donation confirmed"
|
@subject = "Donation confirmed"
|
||||||
mail to: @user.email, subject: @subject
|
send_mail
|
||||||
end
|
end
|
||||||
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