Move time zone logic into mailers

This commit is contained in:
Christian Schmidt 2024-09-12 17:18:40 +02:00
parent 0854edefb7
commit 5d7e12b72d
18 changed files with 102 additions and 72 deletions

View file

@ -14,7 +14,7 @@ class AdminMailer < ApplicationMailer
def new_report(report)
@report = report
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance, id: @report.id)
end
end
@ -22,7 +22,7 @@ class AdminMailer < ApplicationMailer
def new_appeal(appeal)
@appeal = appeal
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance, username: @appeal.account.username)
end
end
@ -30,7 +30,7 @@ class AdminMailer < ApplicationMailer
def new_pending_account(user)
@account = user.account
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance, username: @account.username)
end
end
@ -40,7 +40,7 @@ class AdminMailer < ApplicationMailer
@tags = tags
@statuses = statuses
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance)
end
end
@ -48,7 +48,7 @@ class AdminMailer < ApplicationMailer
def new_software_updates
@software_updates = SoftwareUpdate.all.to_a.sort_by(&:gem_version)
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance)
end
end
@ -60,13 +60,13 @@ class AdminMailer < ApplicationMailer
headers['X-Priority'] = '1'
headers['Importance'] = 'high'
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance)
end
end
def auto_close_registrations
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(instance: @instance)
end
end
@ -75,6 +75,7 @@ class AdminMailer < ApplicationMailer
def process_params
@me = params[:recipient]
@user = @me.user
end
def set_instance

View file

@ -11,8 +11,10 @@ class ApplicationMailer < ActionMailer::Base
protected
def locale_for_account(account, &block)
I18n.with_locale(account.user_locale || I18n.default_locale, &block)
def with_user(user, &block)
I18n.with_locale(user.locale || I18n.default_locale) do
Time.use_zone(user.time_zone || ENV['DEFAULT_TIME_ZONE'] || 'UTC', &block)
end
end
def set_autoreply_headers!

View file

@ -17,7 +17,7 @@ class NotificationMailer < ApplicationMailer
def mention
return unless @user.functional? && @status.present?
locale_for_account(@me) do
with_user(@user) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @status.account.acct)
end
@ -26,7 +26,7 @@ class NotificationMailer < ApplicationMailer
def follow
return unless @user.functional?
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(name: @account.acct)
end
end
@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer
def favourite
return unless @user.functional? && @status.present?
locale_for_account(@me) do
with_user(@user) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct)
end
@ -43,7 +43,7 @@ class NotificationMailer < ApplicationMailer
def reblog
return unless @user.functional? && @status.present?
locale_for_account(@me) do
with_user(@user) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct)
end
@ -52,7 +52,7 @@ class NotificationMailer < ApplicationMailer
def follow_request
return unless @user.functional?
locale_for_account(@me) do
with_user(@user) do
mail subject: default_i18n_subject(name: @account.acct)
end
end

View file

@ -20,7 +20,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale) do
with_user(@resource) do
mail to: @resource.unconfirmed_email.presence || @resource.email,
subject: I18n.t(@resource.pending_reconfirmation? ? 'devise.mailer.reconfirmation_instructions.subject' : 'devise.mailer.confirmation_instructions.subject', instance: @instance),
template_name: @resource.pending_reconfirmation? ? 'reconfirmation_instructions' : 'confirmation_instructions'
@ -33,7 +33,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -43,7 +43,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -53,7 +53,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -63,7 +63,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -83,7 +83,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -93,7 +93,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -103,7 +103,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: default_devise_subject
end
end
@ -114,7 +114,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
end
end
@ -125,7 +125,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale(use_current_locale: true)) do
with_user(@resource, use_current_locale: true) do
mail subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
end
end
@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer
@has_active_relationships = @resource.account.active_relationships.exists?
@has_statuses = @resource.account.statuses.exists?
I18n.with_locale(locale) do
with_user(@resource) do
mail subject: default_i18n_subject
end
end
@ -152,7 +152,7 @@ class UserMailer < Devise::Mailer
return unless @resource.active_for_authentication?
I18n.with_locale(locale) do
with_user(@resource) do
mail subject: default_i18n_subject
end
end
@ -162,7 +162,7 @@ class UserMailer < Devise::Mailer
@warning = warning
@statuses = @warning.statuses.includes(:account, :preloadable_poll, :media_attachments, active_mentions: [:account])
I18n.with_locale(locale) do
with_user(@resource) do
mail subject: I18n.t("user_mailer.warning.subject.#{@warning.action}", acct: "@#{user.account.local_username_and_domain}")
end
end
@ -171,7 +171,7 @@ class UserMailer < Devise::Mailer
@resource = user
@appeal = appeal
I18n.with_locale(locale) do
with_user(@resource) do
mail subject: default_i18n_subject(date: l(@appeal.created_at))
end
end
@ -180,7 +180,7 @@ class UserMailer < Devise::Mailer
@resource = user
@appeal = appeal
I18n.with_locale(locale) do
with_user(@resource) do
mail subject: default_i18n_subject(date: l(@appeal.created_at))
end
end
@ -190,9 +190,10 @@ class UserMailer < Devise::Mailer
@remote_ip = remote_ip
@user_agent = user_agent
@detection = Browser.new(user_agent)
@timestamp = timestamp.to_time.utc
I18n.with_locale(locale) do
with_user(@resource) do
@timestamp = timestamp.in_time_zone
mail subject: default_i18n_subject
end
end
@ -202,9 +203,10 @@ class UserMailer < Devise::Mailer
@remote_ip = remote_ip
@user_agent = user_agent
@detection = Browser.new(user_agent)
@timestamp = timestamp.to_time.utc
I18n.with_locale(locale) do
with_user(@resource) do
@timestamp = timestamp.in_time_zone
mail subject: default_i18n_subject
end
end
@ -219,7 +221,9 @@ class UserMailer < Devise::Mailer
@instance = Rails.configuration.x.local_domain
end
def locale(use_current_locale: false)
@resource.locale.presence || (use_current_locale && I18n.locale) || I18n.default_locale
def with_user(user, use_current_locale: false, &block)
I18n.with_locale(user.locale || (use_current_locale && I18n.locale) || I18n.default_locale) do
Time.use_zone(user.time_zone || ENV['DEFAULT_TIME_ZONE'] || 'UTC', &block)
end
end
end

View file

@ -28,4 +28,4 @@
= link_to a.remote_url, a.remote_url
%p.email-status-footer
= link_to l(status.created_at.in_time_zone(time_zone.presence), format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")
= link_to l(status.created_at, format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")

View file

@ -7,6 +7,6 @@
%tr
%td.email-inner-card-td.email-prose
%p= t 'user_mailer.appeal_approved.explanation',
appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone),
strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone)
appeal_date: l(@appeal.created_at, format: :with_time_zone),
strike_date: l(@appeal.strike.created_at, format: :with_time_zone)
= render 'application/mailer/button', text: t('user_mailer.appeal_approved.action'), url: root_url

View file

@ -2,6 +2,6 @@
===
<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %>
<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at, format: :with_time_zone), strike_date: l(@appeal.strike.created_at, format: :with_time_zone) %>
=> <%= root_url %>

View file

@ -7,6 +7,6 @@
%tr
%td.email-inner-card-td.email-prose
%p= t 'user_mailer.appeal_rejected.explanation',
appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone),
strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone)
appeal_date: l(@appeal.created_at, format: :with_time_zone),
strike_date: l(@appeal.strike.created_at, format: :with_time_zone)
= render 'application/mailer/button', text: t('user_mailer.appeal_approved.action'), url: root_url

View file

@ -2,6 +2,6 @@
===
<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %>
<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at, format: :with_time_zone), strike_date: l(@appeal.strike.created_at, format: :with_time_zone) %>
=> <%= root_url %>

View file

@ -18,7 +18,7 @@
platform: t("sessions.platforms.#{@detection.platform.id}", default: @detection.platform.id.to_s)
%br/
%strong #{t('sessions.date')}:
= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone)
= l(@timestamp, format: :with_time_zone)
= render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url
%p= t 'user_mailer.failed_2fa.further_actions_html',
action: link_to(t('user_mailer.suspicious_sign_in.change_password'), edit_user_registration_url)

View file

@ -8,7 +8,7 @@
<%= t('sessions.ip') %>: <%= @remote_ip %>
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
<%= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %>
<%= l(@timestamp, format: :with_time_zone) %>
<%= t 'user_mailer.failed_2fa.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %>

View file

@ -18,7 +18,7 @@
platform: t("sessions.platforms.#{@detection.platform.id}", default: @detection.platform.id.to_s)
%br/
%strong #{t('sessions.date')}:
= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone)
= l(@timestamp, format: :with_time_zone)
= render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url
%p= t 'user_mailer.suspicious_sign_in.further_actions_html',
action: link_to(t('user_mailer.suspicious_sign_in.change_password'), edit_user_registration_url)

View file

@ -8,7 +8,7 @@
<%= t('sessions.ip') %>: <%= @remote_ip %>
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
<%= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %>
<%= l(@timestamp, format: :with_time_zone) %>
<%= t 'user_mailer.suspicious_sign_in.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %>

View file

@ -78,13 +78,12 @@ module Mastodon
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
config.time_zone = ENV['DEFAULT_TIME_ZONE'] || 'UTC'
config.active_job.queue_adapter = :sidekiq
config.action_mailer.deliver_later_queue_name = 'mailers'

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# Raise if invalid zone is specified
Time.find_zone!(ENV.fetch('DEFAULT_TIME_ZONE', nil))

View file

@ -5,8 +5,8 @@ require 'rails_helper'
RSpec.describe NotificationMailer do
let(:receiver) { Fabricate(:user, account_attributes: { username: 'alice' }) }
let(:sender) { Fabricate(:account, username: 'bob') }
let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }
let(:own_status) { Fabricate(:status, account: receiver.account, text: 'The body of the own status') }
let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status', created_at: '2024-01-01 12:01Z') }
let(:own_status) { Fabricate(:status, account: receiver.account, text: 'The body of the own status', created_at: '2024-01-01 13:02Z') }
shared_examples 'standard headers' do |type|
it 'renders the email' do
@ -38,6 +38,7 @@ RSpec.describe NotificationMailer do
let(:mail) { prepared_mailer_for(receiver.account).mention }
include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
include_examples 'timestamp in time zone', '2024-01-01 12:01Z'.to_datetime
include_examples 'standard headers', 'mention'
include_examples 'thread headers'
@ -72,6 +73,7 @@ RSpec.describe NotificationMailer do
let(:mail) { prepared_mailer_for(own_status.account).favourite }
include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
include_examples 'timestamp in time zone', '2024-01-01 13:02Z'.to_datetime
include_examples 'standard headers', 'favourite'
include_examples 'thread headers'
@ -90,6 +92,7 @@ RSpec.describe NotificationMailer do
let(:mail) { prepared_mailer_for(own_status.account).reblog }
include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
include_examples 'timestamp in time zone', '2024-01-01 13:02Z'.to_datetime
include_examples 'standard headers', 'reblog'
include_examples 'thread headers'

View file

@ -3,14 +3,12 @@
require 'rails_helper'
RSpec.describe UserMailer do
let(:receiver) { Fabricate(:user) }
let(:receiver) { Fabricate(:user, locale: nil) }
describe '#confirmation_instructions' do
let(:mail) { described_class.confirmation_instructions(receiver, 'spec') }
it 'renders confirmation instructions' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('devise.mailer.confirmation_instructions.title')))
@ -45,8 +43,6 @@ RSpec.describe UserMailer do
let(:mail) { described_class.reset_password_instructions(receiver, 'spec') }
it 'renders reset password instructions' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('devise.mailer.reset_password_instructions.title')))
@ -61,8 +57,6 @@ RSpec.describe UserMailer do
let(:mail) { described_class.password_change(receiver) }
it 'renders password change notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('devise.mailer.password_change.title')))
@ -76,8 +70,6 @@ RSpec.describe UserMailer do
let(:mail) { described_class.email_changed(receiver) }
it 'renders email change notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('devise.mailer.email_changed.title')))
@ -92,8 +84,6 @@ RSpec.describe UserMailer do
let(:mail) { described_class.warning(receiver, strike) }
it 'renders warning notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('user_mailer.warning.title.suspend', acct: receiver.account.acct)))
@ -106,8 +96,6 @@ RSpec.describe UserMailer do
let(:mail) { described_class.webauthn_credential_deleted(receiver, credential) }
it 'renders webauthn credential deleted notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('devise.mailer.webauthn_credential.deleted.title')))
@ -120,12 +108,10 @@ RSpec.describe UserMailer do
describe '#suspicious_sign_in' do
let(:ip) { '192.168.0.1' }
let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' }
let(:timestamp) { Time.now.utc }
let(:timestamp) { '2024-01-01 12:34Z'.to_datetime }
let(:mail) { described_class.suspicious_sign_in(receiver, ip, agent, timestamp) }
it 'renders suspicious sign in notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('user_mailer.suspicious_sign_in.explanation')))
@ -133,17 +119,16 @@ RSpec.describe UserMailer do
include_examples 'localized subject',
'user_mailer.suspicious_sign_in.subject'
include_examples 'timestamp in time zone', '2024-01-01 12:34Z'.to_datetime
end
describe '#failed_2fa' do
let(:ip) { '192.168.0.1' }
let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' }
let(:timestamp) { Time.now.utc }
let(:timestamp) { '2024-01-01 12:34Z'.to_datetime }
let(:mail) { described_class.failed_2fa(receiver, ip, agent, timestamp) }
it 'renders failed 2FA notification' do
receiver.update!(locale: nil)
expect(mail)
.to be_present
.and(have_body_text(I18n.t('user_mailer.failed_2fa.explanation')))
@ -151,10 +136,11 @@ RSpec.describe UserMailer do
include_examples 'localized subject',
'user_mailer.failed_2fa.subject'
include_examples 'timestamp in time zone', '2024-01-01 12:34Z'.to_datetime
end
describe '#appeal_approved' do
let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) }
let(:appeal) { Fabricate(:appeal, account: receiver.account, created_at: '2024-01-01 12:34Z', approved_at: Time.now.utc) }
let(:mail) { described_class.appeal_approved(receiver, appeal) }
it 'renders appeal_approved notification' do
@ -163,10 +149,12 @@ RSpec.describe UserMailer do
.and(have_subject(I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at))))
.and(have_body_text(I18n.t('user_mailer.appeal_approved.title')))
end
include_examples 'timestamp in time zone', '2024-01-01 12:34Z'.to_datetime
end
describe '#appeal_rejected' do
let(:appeal) { Fabricate(:appeal, account: receiver.account, rejected_at: Time.now.utc) }
let(:appeal) { Fabricate(:appeal, account: receiver.account, created_at: '2024-01-01 12:34Z', rejected_at: Time.now.utc) }
let(:mail) { described_class.appeal_rejected(receiver, appeal) }
it 'renders appeal_rejected notification' do
@ -175,6 +163,8 @@ RSpec.describe UserMailer do
.and(have_subject(I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at))))
.and(have_body_text(I18n.t('user_mailer.appeal_rejected.title')))
end
include_examples 'timestamp in time zone', '2024-01-01 12:34Z'.to_datetime
end
describe '#two_factor_enabled' do

View file

@ -12,3 +12,30 @@ RSpec.shared_examples 'localized subject' do |*args, **kwrest|
expect(mail.subject).to eq I18n.t(*args, **kwrest.merge(locale: I18n.default_locale))
end
end
RSpec.shared_examples 'timestamp in time zone' do |at|
context 'when default time zone is defined' do
around do |example|
ClimateControl.modify DEFAULT_TIME_ZONE: 'Europe/Athens' do
example.run
end
end
it 'displays timestamp in time zone of the receiver' do
time_zone = 'Europe/Berlin'
receiver.update!(time_zone: time_zone)
expect(mail)
.to have_body_text(at.in_time_zone(time_zone).strftime(I18n.t('time.formats.with_time_zone')))
end
it 'displays timestamp in default time zone if the time zone of the receiver is unavailable' do
receiver.update!(time_zone: nil)
expect(mail).to have_body_text(at.in_time_zone('Europe/Athens').strftime(I18n.t('time.formats.with_time_zone')))
end
end
it 'formats timestamp in UTC' do
receiver.update!(time_zone: nil)
expect(mail).to have_body_text(at.in_time_zone('UTC').strftime(I18n.t('time.formats.with_time_zone')))
end
end