Encrypt all system emails for users with PGP key #207
| @ -1,3 +1,90 @@ | ||||
| class ApplicationMailer < ActionMailer::Base | ||||
|   default Rails.application.config.action_mailer.default_options | ||||
|   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 | ||||
|       self.class.default[:from] | ||||
|     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 | ||||
| @ -57,16 +57,22 @@ Rails.application.configure do | ||||
|   # routes, locales, etc. This feature depends on the listen gem. | ||||
|   config.file_watcher = ActiveSupport::EventedFileUpdateChecker | ||||
| 
 | ||||
|   config.action_mailer.default_options = { | ||||
|     from: "accounts@localhost" | ||||
|   } | ||||
| 
 | ||||
|   # Don't actually send emails, cache them for viewing via letter opener | ||||
|   config.action_mailer.delivery_method = :letter_opener | ||||
| 
 | ||||
|   # Don't care if the mailer can't send | ||||
|   config.action_mailer.raise_delivery_errors = true | ||||
| 
 | ||||
|   # Base URL to be used by email template link helpers | ||||
|   config.action_mailer.default_url_options = { host: "localhost:3000", protocol: "http" } | ||||
|   config.action_mailer.default_url_options = { | ||||
|     host: "localhost:3000", | ||||
|     protocol: "http" | ||||
|   } | ||||
| 
 | ||||
|   config.action_mailer.default_options = { | ||||
|     from: "accounts@localhost", | ||||
|     message_id: -> { "<#{Mail.random_tag}@localhost>" }, | ||||
|   } | ||||
| 
 | ||||
|   # Allow requests from any IP | ||||
|   config.web_console.permissions = '0.0.0.0/0' | ||||
|  | ||||
| @ -63,7 +63,7 @@ Rails.application.configure do | ||||
|   outgoing_email_domain  = Mail::Address.new(outgoing_email_address).domain | ||||
| 
 | ||||
|   config.action_mailer.default_url_options = { | ||||
|     host: ENV['AKKOUNTS_DOMAIN'], | ||||
|     host: ENV.fetch('AKKOUNTS_DOMAIN'), | ||||
|     protocol: "https", | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -46,8 +46,12 @@ Rails.application.configure do | ||||
| 
 | ||||
|   config.action_mailer.default_url_options = { | ||||
|     host: "accounts.kosmos.org", | ||||
|     protocol: "https", | ||||
|     from: "accounts@kosmos.org" | ||||
|     protocol: "https" | ||||
|   } | ||||
| 
 | ||||
|   config.action_mailer.default_options = { | ||||
|     from: "accounts@kosmos.org", | ||||
|     message_id: -> { "<#{Mail.random_tag}@kosmos.org>" }, | ||||
|   } | ||||
| 
 | ||||
|   config.active_job.queue_adapter = :test | ||||
|  | ||||
							
								
								
									
										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