diff --git a/app/mailers/emails/admin_notification.rb b/app/mailers/emails/admin_notification.rb index e11f06d8fc94d66ae325839688b85267108808dd..f44dd448a35567904d03c70a5e3f0c8cde242ad3 100644 --- a/app/mailers/emails/admin_notification.rb +++ b/app/mailers/emails/admin_notification.rb @@ -15,5 +15,18 @@ def send_unsubscribed_notification(user_id) email = user.notification_email_or_default mail to: email, subject: "Unsubscribed from GitLab administrator notifications" end + + def user_auto_banned_email(admin_id, user_id, max_project_downloads:, within_seconds:) + admin = User.find(admin_id) + @user = User.find(user_id) + @max_project_downloads = max_project_downloads + @within_minutes = within_seconds / 60 + + Gitlab::I18n.with_locale(admin.preferred_language) do + email_with_layout( + to: admin.notification_email_or_default, + subject: subject(_("We've detected unusual activity"))) + end + end end end diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb index 9705a3052d49dc92a887ce87be0e3842a90883ba..d10ba40d225ab32470b8c0f34dbfea769c5b80a3 100644 --- a/app/mailers/emails/auto_devops.rb +++ b/app/mailers/emails/auto_devops.rb @@ -8,11 +8,9 @@ def autodevops_disabled_email(pipeline, recipient) add_project_headers - mail(to: recipient, - subject: auto_devops_disabled_subject(@project.name)) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: recipient, + subject: auto_devops_disabled_subject(@project.name)) end private diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index bbc4be3b3243d73f616ae89f33c9f5404338700f..6a5680c080bd363d1a48f2567014d4e8a467d971 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -94,10 +94,9 @@ def import_issues_csv_email(user_id, project_id, results) @project = Project.find(project_id) @results = results - mail(to: @user.notification_email_for(@project.group), subject: subject('Imported issues')) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: @user.notification_email_for(@project.group), + subject: subject('Imported issues')) end def issues_csv_email(user, project, csv_data, export_status) @@ -110,10 +109,9 @@ def issues_csv_email(user, project, csv_data, export_status) filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } - mail(to: user.notification_email_for(@project.group), subject: subject("Exported issues")) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: user.notification_email_for(@project.group), + subject: subject("Exported issues")) end private diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index ef2220751bf40b336e351f27321fae4a77fc70e1..c885e41671c2779e329499aa5edcddb6b287ed5d 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -21,7 +21,7 @@ def member_access_requested_email(member_source_type, member_id, recipient_id) user = User.find(recipient_id) - member_email_with_layout( + email_with_layout( to: user.notification_email_for(notification_group), subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}")) end @@ -32,7 +32,7 @@ def member_access_granted_email(member_source_type, member_id) return unless member_exists? - member_email_with_layout( + email_with_layout( to: member.user.notification_email_for(notification_group), subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted")) end @@ -47,7 +47,7 @@ def member_access_denied_email(member_source_type, source_id, user_id) human_name = @source_hidden ? 'Hidden' : member_source.human_name - member_email_with_layout( + email_with_layout( to: user.notification_email_for(notification_group), subject: subject("Access to the #{human_name} #{member_source.model_name.singular} was denied")) end @@ -83,7 +83,7 @@ def member_invited_reminder_email(member_source_type, member_id, token, reminder subject_line = subjects[reminder_index] % { inviter: member.created_by.name } - member_email_with_layout( + email_with_layout( layout: 'unknown_user_mailer', to: member.invite_email, subject: subject(subject_line) @@ -97,7 +97,7 @@ def member_invite_accepted_email(member_source_type, member_id) return unless member_exists? return unless member.created_by - member_email_with_layout( + email_with_layout( to: member.created_by.notification_email_for(notification_group), subject: subject('Invitation accepted')) end @@ -111,7 +111,7 @@ def member_invite_declined_email(member_source_type, source_id, invite_email, cr user = User.find(created_by_id) - member_email_with_layout( + email_with_layout( to: user.notification_email_for(notification_group), subject: subject('Invitation declined')) end @@ -128,7 +128,7 @@ def member_expiration_date_updated_email(member_source_type, member_id) _('Group membership expiration date removed') end - member_email_with_layout( + email_with_layout( to: member.user.notification_email_for(notification_group), subject: subject(subject)) end @@ -176,13 +176,6 @@ def member_exists? def member_source_class @member_source_type.classify.constantize end - - def member_email_with_layout(to:, subject:, layout: 'mailer') - mail(to: to, subject: subject) do |format| - format.html { render layout: layout } - format.text { render layout: layout } - end - end end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 5cbc3c9ef9c852a3df5c167bcdde6051240def90..83d37a365de7a1e820fe815d4c2026e19b615d74 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -149,10 +149,9 @@ def merge_requests_csv_email(user, project, csv_data, export_status) filename = "#{project.full_path.parameterize}_merge_requests_#{Date.current.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } - mail(to: user.notification_email_for(@project.group), subject: subject("Exported merge requests")) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: user.notification_email_for(@project.group), + subject: subject("Exported merge requests")) end def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 5363ad637715f424d9eca72cfbcbb79a0d62c4fd..463f5d3943ab2274c9f8b7b8b0d8b168d65814df 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -30,11 +30,9 @@ def pipeline_mail(pipeline, recipient, status) add_headers - mail(to: recipient, - subject: subject(pipeline_subject(status))) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: recipient, + subject: subject(pipeline_subject(status))) end def add_headers diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 31fcc7c15cb8e3cb52b1dda0cbc58c7f0ab019c7..81f082b9680fa3a6c57323d9340c51fa813e3af2 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -13,7 +13,7 @@ def instance_access_request_email(user, recipient) @user = user @recipient = recipient - profile_email_with_layout( + email_with_layout( to: recipient.notification_email_or_default, subject: subject(_("GitLab Account Request"))) end @@ -21,7 +21,7 @@ def instance_access_request_email(user, recipient) def user_admin_rejection_email(name, email) @name = name - profile_email_with_layout( + email_with_layout( to: email, subject: subject(_("GitLab account request rejected"))) end @@ -29,7 +29,7 @@ def user_admin_rejection_email(name, email) def user_deactivated_email(name, email) @name = name - profile_email_with_layout( + email_with_layout( to: email, subject: subject(_('Your account has been deactivated'))) end @@ -125,7 +125,7 @@ def unknown_sign_in_email(user, ip, time) @target_url = edit_profile_password_url Gitlab::I18n.with_locale(@user.preferred_language) do - profile_email_with_layout( + email_with_layout( to: @user.notification_email_or_default, subject: subject(_("%{host} sign-in from new location") % { host: Gitlab.config.gitlab.host })) end @@ -151,15 +151,6 @@ def new_email_address_added_email(user, email) mail(to: @user.notification_email_or_default, subject: subject(_("New email address added"))) end end - - private - - def profile_email_with_layout(to:, subject:, layout: 'mailer') - mail(to: to, subject: subject) do |format| - format.html { render layout: layout } - format.text { render layout: layout } - end - end end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index efc6ce163c04066c6ebddbc02e022841c273d8d6..ed3fa28b15fe5e315aedfefcb5f96cd69a8877ba 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -75,11 +75,9 @@ def inactive_project_deletion_warning_email(project, user, deletion_date) subject_text = "Action required: Project #{project.name} is scheduled to be deleted on " \ "#{deletion_date} due to inactivity" - mail(to: user.notification_email_for(project.group), - subject: subject(subject_text)) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: user.notification_email_for(project.group), + subject: subject(subject_text)) end private diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 03b70fffde1cc88e50160a49c8fe7d3d57d72c4c..b70ce1d365502f6f3958f758b1e79798e956e84f 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -222,6 +222,13 @@ def add_unsubscription_headers_and_links headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',') @unsubscribe_url = unsubscribe_sent_notification_url(@sent_notification) end + + def email_with_layout(to:, subject:, layout: 'mailer') + mail(to: to, subject: subject) do |format| + format.html { render layout: layout } + format.text { render layout: layout } + end + end end Notify.prepend_mod_with('Notify') diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 60d594651655d4ec41372a91f4994bb626bb9983..61456ef79c82b7b754c7ff6ba238d506d78af379 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -205,6 +205,10 @@ def inactive_project_deletion_warning Notify.inactive_project_deletion_warning_email(project, user, '2022-04-22').message end + def user_auto_banned_email + ::Notify.user_auto_banned_email(user.id, user.id, max_project_downloads: 5, within_seconds: 600).message + end + private def project diff --git a/app/views/notify/user_auto_banned_email.html.haml b/app/views/notify/user_auto_banned_email.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..d88c06526eb918e69b83565a266725812b2bd2f5 --- /dev/null +++ b/app/views/notify/user_auto_banned_email.html.haml @@ -0,0 +1,9 @@ +- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe +- link_end = '</a>'.html_safe += email_default_heading(_("We've detected some unusual activity")) +%p + = _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes } +%p + = _('If this is a mistake, you can %{link_start}unban them%{link_end}.').html_safe % { link_start: link_start % { url: admin_users_url(filter: 'banned') }, link_end: link_end } +%p + = _('You can adjust rules on auto-banning %{link_start}here%{link_end}.').html_safe % { link_start: link_start % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') }, link_end: link_end } diff --git a/app/views/notify/user_auto_banned_email.text.erb b/app/views/notify/user_auto_banned_email.text.erb new file mode 100644 index 0000000000000000000000000000000000000000..0469ee9788ce0a8aa48bf62c4dc04b019ef7e194 --- /dev/null +++ b/app/views/notify/user_auto_banned_email.text.erb @@ -0,0 +1,7 @@ +<%= _("We've detected some unusual activity") %> + +<%= _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes } %> + +<%= _('If this is a mistake, you can unban them: %{url}.') % { url: admin_users_url(filter: 'banned') } %> + +<%= _('You can adjust rules on auto-banning here: %{url}.') % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') } %> diff --git a/ee/app/mailers/ee/emails/members.rb b/ee/app/mailers/ee/emails/members.rb index 0b4db548e7a7214c7e4e082027e587d09af1ab42..98b35736e3bf33ab73e48a0fdb2f19e330d11bb5 100644 --- a/ee/app/mailers/ee/emails/members.rb +++ b/ee/app/mailers/ee/emails/members.rb @@ -10,7 +10,7 @@ def provisioned_member_access_granted_email(member_id) @user = member.user - member_email_with_layout( + email_with_layout( to: member.user.email, subject: subject("Welcome to GitLab")) end diff --git a/ee/app/mailers/ee/emails/projects.rb b/ee/app/mailers/ee/emails/projects.rb index 7d7930b5bf55dd580c6d3b62052d6b9ed5ac50d8..3a27827fc45731c9e61f30b0f4d49bfd5b009eaf 100644 --- a/ee/app/mailers/ee/emails/projects.rb +++ b/ee/app/mailers/ee/emails/projects.rb @@ -36,10 +36,9 @@ def user_escalation_rule_deleted_email(user, project, rules, recipient) @project = project @rules = rules - mail(to: recipient.notification_email_for(@project.group), subject: subject('User removed from escalation policy')) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: recipient.notification_email_for(@project.group), + subject: subject('User removed from escalation policy')) end def incident_escalation_fired_email(project, user, issue) diff --git a/ee/app/mailers/emails/group_memberships.rb b/ee/app/mailers/emails/group_memberships.rb index 3bbb75b62ce9082deffb896f6ccb104c250376eb..3772aaa07833db415493de6c61838e9225e3ea1d 100644 --- a/ee/app/mailers/emails/group_memberships.rb +++ b/ee/app/mailers/emails/group_memberships.rb @@ -7,10 +7,9 @@ def memberships_export_email(csv_data:, requested_by:, group:) filename = "#{group.full_path.parameterize}_group_memberships_#{Date.current.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } - mail(to: requested_by.notification_email_for(group), subject: "Exported group membership list") do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: requested_by.notification_email_for(group), + subject: "Exported group membership list") end end end diff --git a/ee/app/mailers/emails/oncall_rotation.rb b/ee/app/mailers/emails/oncall_rotation.rb index fb7a91809274c228b885c23d83686ea7ee9b5187..df5213eab9de1fdbff6e2f0f8a53859f48140ac2 100644 --- a/ee/app/mailers/emails/oncall_rotation.rb +++ b/ee/app/mailers/emails/oncall_rotation.rb @@ -10,10 +10,9 @@ def user_removed_from_rotation_email(user, rotation, recipients) @schedule = rotation.schedule @project = rotation.project - mail(to: recipients.map(&:email), subject: subject('User removed from On-call rotation')) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: recipients.map(&:email), + subject: subject('User removed from On-call rotation')) end end end diff --git a/ee/app/mailers/emails/requirements.rb b/ee/app/mailers/emails/requirements.rb index 0460755a31bd9c4e7c8403de20e17ccbe00ef745..54475a477ad36d58b3eea81588944895ec9dbb66 100644 --- a/ee/app/mailers/emails/requirements.rb +++ b/ee/app/mailers/emails/requirements.rb @@ -7,7 +7,9 @@ def import_requirements_csv_email(user_id, project_id, results) @project = Project.find(project_id) @results = results - requirement_email_with_layout(@user, @project.group, _('Imported requirements')) + email_with_layout( + to: @user.notification_email_for(@project.group), + subject: subject(_('Imported requirements'))) end def requirements_csv_email(user, project, csv_data, export_status) @@ -18,14 +20,9 @@ def requirements_csv_email(user, project, csv_data, export_status) filename = "#{project.full_path.parameterize}_requirements_#{Date.current.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } - requirement_email_with_layout(user, @project.group, _('Exported requirements')) - end - - def requirement_email_with_layout(user, group, subj) - mail(to: user.notification_email_for(group), subject: subject(subj)) do |format| - format.html { render layout: 'mailer' } - format.text { render layout: 'mailer' } - end + email_with_layout( + to: user.notification_email_for(@project.group), + subject: subject(_('Exported requirements'))) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4171fc4687f487871d59381264af991412e4cdf3..314af57bb857987e28cd1d1d33309ffa571952b5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19162,6 +19162,12 @@ msgstr "" msgid "If this email was added in error, you can remove it here: %{profile_emails_url}" msgstr "" +msgid "If this is a mistake, you can %{link_start}unban them%{link_end}." +msgstr "" + +msgid "If this is a mistake, you can unban them: %{url}." +msgstr "" + msgid "If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}." msgstr "" @@ -42441,6 +42447,9 @@ msgstr "" msgid "We want to be sure it is you, please confirm you are not a robot." msgstr "" +msgid "We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes." +msgstr "" + msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders." msgstr "" @@ -42456,6 +42465,12 @@ msgstr "" msgid "We're experiencing difficulties and this tab content is currently unavailable." msgstr "" +msgid "We've detected some unusual activity" +msgstr "" + +msgid "We've detected unusual activity" +msgstr "" + msgid "We've found no vulnerabilities" msgstr "" @@ -43276,6 +43291,12 @@ msgstr "" msgid "You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}." msgstr "" +msgid "You can adjust rules on auto-banning %{link_start}here%{link_end}." +msgstr "" + +msgid "You can adjust rules on auto-banning here: %{url}." +msgstr "" + msgid "You can also create a project from the command line." msgstr "" diff --git a/spec/mailers/emails/admin_notification_spec.rb b/spec/mailers/emails/admin_notification_spec.rb index 90381eb8ffdafe378c36ef852685ff6e00a16a00..a233be86a83dce1bf03b8c3b8a7a3735c40824c2 100644 --- a/spec/mailers/emails/admin_notification_spec.rb +++ b/spec/mailers/emails/admin_notification_spec.rb @@ -3,9 +3,62 @@ require 'spec_helper' RSpec.describe Emails::AdminNotification do + include EmailSpec::Matchers + include_context 'gitlab email notification' + it 'adds email methods to Notify' do subject.instance_methods.each do |email_method| expect(Notify).to be_respond_to(email_method) end end + + describe 'user_auto_banned_email' do + let_it_be(:admin) { create(:user) } + let_it_be(:user) { create(:user) } + + let(:max_project_downloads) { 5 } + let(:time_period) { 600 } + + subject do + Notify.user_auto_banned_email( + admin.id, user.id, + max_project_downloads: max_project_downloads, + within_seconds: time_period + ) + end + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'appearance header and footer enabled' + it_behaves_like 'appearance header and footer not enabled' + + it 'is sent to the administrator' do + is_expected.to deliver_to admin.email + end + + it 'has the correct subject' do + is_expected.to have_subject "We've detected unusual activity" + end + + it 'includes the name of the user' do + is_expected.to have_body_text user.name + end + + it 'includes the reason' do + is_expected.to have_body_text "due to them downloading more than 5 project repositories within 10 minutes" + end + + it 'includes a link to unban the user' do + is_expected.to have_body_text admin_users_url(filter: 'banned') + end + + it 'includes a link to change the settings' do + is_expected.to have_body_text network_admin_application_settings_url(anchor: 'js-ip-limits-settings') + end + + it 'includes the email reason' do + is_expected.to have_body_text "You're receiving this email because of your account on localhost" + end + end end