diff --git a/app/controllers/admin/email_subscriptions/additional_footer_texts_controller.rb b/app/controllers/admin/email_subscriptions/additional_footer_texts_controller.rb new file mode 100644 index 0000000000..fcc774a256 --- /dev/null +++ b/app/controllers/admin/email_subscriptions/additional_footer_texts_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::EmailSubscriptions::AdditionalFooterTextsController < Admin::SettingsController + private + + def after_update_redirect_path + admin_email_subscriptions_path + end +end diff --git a/app/controllers/admin/email_subscriptions/setups_controller.rb b/app/controllers/admin/email_subscriptions/setups_controller.rb new file mode 100644 index 0000000000..81f6267171 --- /dev/null +++ b/app/controllers/admin/email_subscriptions/setups_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Admin::EmailSubscriptions::SetupsController < Admin::BaseController + before_action :require_enabled! + + def show + authorize :email_subscription, :enable? + + @form = Form::EmailSubscriptionsConfirmation.new + end + + def create + authorize :email_subscription, :enable? + + @form = Form::EmailSubscriptionsConfirmation.new(resource_params) + + if @form.valid? + Setting.email_subscriptions = true + redirect_to admin_email_subscriptions_path + else + render :show + end + end + + private + + def require_enabled! + raise ActionController::RoutingError, 'Feature disabled' unless Rails.application.config.x.email_subscriptions + end + + def resource_params + params.expect(form_email_subscriptions_confirmation: [:agreement_email_volume, :agreement_privacy_and_terms]) + end +end diff --git a/app/controllers/admin/email_subscriptions_controller.rb b/app/controllers/admin/email_subscriptions_controller.rb new file mode 100644 index 0000000000..39a32909df --- /dev/null +++ b/app/controllers/admin/email_subscriptions_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Admin::EmailSubscriptionsController < Admin::BaseController + def index + authorize :email_subscription, :index? + + @enabled = Setting.email_subscriptions + @roles = UserRole.where('permissions & ? != 0', UserRole::FLAGS[:manage_email_subscriptions] | UserRole::FLAGS[:administrator]) + @accounts = Account.local.joins(:email_subscriptions).where.associated(:email_subscriptions).includes(:user) + end + + def disable + authorize :email_subscription, :disable? + Setting.email_subscriptions = false + redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.disabled_msg') + end + + def purge + authorize :email_subscription, :purge? + Admin::EmailSubscriptionsPurgeWorker.perform_async + redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.purged_msg') + end +end diff --git a/app/controllers/api/v1/accounts/email_subscriptions_controller.rb b/app/controllers/api/v1/accounts/email_subscriptions_controller.rb index 4e773f902b..bf7a1447e1 100644 --- a/app/controllers/api/v1/accounts/email_subscriptions_controller.rb +++ b/app/controllers/api/v1/accounts/email_subscriptions_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Accounts::EmailSubscriptionsController < Api::BaseController end def require_feature_enabled! - head 404 unless Mastodon::Feature.email_subscriptions_enabled? + head 404 unless Rails.application.config.x.email_subscriptions && Setting.email_subscriptions end def require_account_permissions! diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6fc97ab5ed..c2d8c46506 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -127,6 +127,10 @@ module ApplicationHelper ) end + def emptyphaunt + inline_svg_tag 'elephant_ui.svg' + end + def check_icon inline_svg_tag 'check.svg' end diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 47bc541cc9..f2fe35d13e 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -2223,3 +2223,117 @@ a.sparkline { } } } + +.subheading-with-action { + display: flex; + align-items: flex-start; + margin-bottom: 16px; + + &__subheading { + flex-grow: 1; + } + + h3 { + font-size: 20px; + font-weight: 600; + margin-bottom: 8px; + } + + p { + font-size: 16px; + } +} + +.actions-list { + &__item { + border-bottom: 1px solid var(--color-border-primary); + padding: 12px 0; + display: flex; + gap: 16px; + align-items: center; + + &__label { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 2px; + + &__label { + font-weight: 600; + font-size: 15px; + } + + &__hint { + font-size: 13px; + color: var(--color-text-secondary); + } + } + + &__action { + flex-shrink: 0; + } + } +} + +.empty-state { + border: 1px solid var(--color-border-primary); + border-radius: 12px; + padding: 24px 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + + &__title-and-description { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + line-height: 21px; + font-size: 15px; + text-align: center; + + &__title { + font-weight: 600; + } + } + + svg { + width: 200px; + } + + // FIXME: This is duplicated with empty_message.module.scss + [data-color-scheme='dark'] & svg { + --color-skin-1: #3a3a50; + --color-skin-2: #67678e; + --color-skin-3: #44445f; + --color-outline: #21212c; + --color-shadow: #181820; + --color-highlight: #b2b1c8; + } +} + +.heading-with-lead { + display: flex; + flex-direction: column; + gap: 8px; + + h1 { + font-size: 28px; + font-weight: 600; + } + + .lead { + font-size: 18px; + color: var(--color-text-secondary); + } +} + +.status-badge { + display: inline-flex; + padding: 3px 4px; + border-radius: 8px; + background: var(--color-bg-success-softest); + font-size: 13px; + font-weight: 600; +} diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 7d2af8ddc1..27b2a46223 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -71,6 +71,24 @@ code { } .simple_form { + h3 { + font-size: 20px; + font-weight: 600; + margin-bottom: 16px; + } + + .numbered-list { + list-style: decimal; + font-size: 16px; + line-height: 24px; + margin-bottom: 16px; + margin-inline-start: 28px; + + li { + margin-bottom: 4px; + } + } + &.hidden { display: none; } diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 1088781417..0023ea353c 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -21,6 +21,24 @@ } } + .align-end { + text-align: end; + } + + .valign-middle { + vertical-align: middle; + } + + .avatar-column { + width: 24px; + + .avatar { + border-radius: 8px; + border: 1px solid var(--color-border-primary); + background: var(--color-background-secondary); + } + } + & > thead > tr > th { vertical-align: bottom; font-weight: 500; @@ -170,6 +188,14 @@ a.table-action-link { } } +.table-icon-link { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + aspect-ratio: 1; +} + .batch-table { &--no-toolbar { .batch-table__toolbar { diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 35e35d5ad5..0ad5335419 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -46,6 +46,7 @@ class Form::AdminSettings remote_topic_feed_access landing_page wrapstodon + email_footer_text ).freeze INTEGER_KEYS = %i( diff --git a/app/models/form/email_subscriptions_confirmation.rb b/app/models/form/email_subscriptions_confirmation.rb new file mode 100644 index 0000000000..e82fd88762 --- /dev/null +++ b/app/models/form/email_subscriptions_confirmation.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Form::EmailSubscriptionsConfirmation + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :agreement_email_volume, :boolean + attribute :agreement_privacy_and_terms, :boolean + + validates :agreement_email_volume, :agreement_privacy_and_terms, acceptance: true +end diff --git a/app/policies/email_subscription_policy.rb b/app/policies/email_subscription_policy.rb new file mode 100644 index 0000000000..201bd72c13 --- /dev/null +++ b/app/policies/email_subscription_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class EmailSubscriptionPolicy < ApplicationPolicy + def index? + role.can?(:manage_settings) + end + + alias enable? index? + + alias disable? index? + + alias purge? index? +end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 9fc5acee9d..8afe3f3b67 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -22,7 +22,7 @@ class REST::AccountSerializer < ActiveModel::Serializer attribute :memorial, if: :memorial? attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? } - attribute :email_subscriptions, if: -> { Mastodon::Feature.email_subscriptions_enabled? } + attribute :email_subscriptions, if: -> { Rails.application.config.x.email_subscriptions && Setting.email_subscriptions } class AccountDecorator < SimpleDelegator def self.model_name diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 3ef1502cd6..9f82de40d3 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -171,7 +171,7 @@ class PostStatusService < BaseService end def process_email_subscriptions! - return unless Mastodon::Feature.email_subscriptions_enabled? && + return unless Rails.application.config.x.email_subscriptions && Setting.email_subscriptions && @status.public_visibility? && (!@status.reply? || @status.in_reply_to_account_id == @status.account_id) && @status.account.user_can?(:manage_email_subscriptions) && @status.account.user_email_subscriptions_enabled? diff --git a/app/views/admin/email_subscriptions/_accounts.html.haml b/app/views/admin/email_subscriptions/_accounts.html.haml new file mode 100644 index 0000000000..d560d417db --- /dev/null +++ b/app/views/admin/email_subscriptions/_accounts.html.haml @@ -0,0 +1,44 @@ +.subheading-with-action + .subheading-with-action__subheading + %h3= t('.title') + %p= t('.lead') + +.table-wrapper + - if accounts.empty? + .empty-state + = emptyphaunt + + .empty-state__title-and-description + .empty-state__title-and-description__title + = t('.empty.no_lists_yet') + .empty-state__title-and-description__description + = t('.empty.hint') + - else + %table.table + %thead + %tr + %th.avatar-column + %th= t('.account') + %th= t('.status') + %th= t('.subscribers') + %th= t('.last_email') + %th + %tbody + - accounts.each do |account| + %tr + %td.valign-middle.avatar-column + = image_tag(full_asset_url(account.avatar_original_url), width: 24, height: 24, alt: '', class: 'avatar') + %td.valign-middle + %strong + %bdi= display_name(account) + %td.valign-middle + - if account.user_can?(:manage_email_subscriptions) && account.user_email_subscriptions_enabled? + %span.status-badge.positive= t('.active') + - else + %span.status-badge.negative= t('.inactive') + %td.valign-middle + = account.email_subscriptions.count + %td.valign-middle + = l account.last_status_at + %td.valign-middle.align-end + = link_to material_symbol('chevron_right'), admin_account_path(account.id), class: 'table-icon-link' diff --git a/app/views/admin/email_subscriptions/_compliance_settings.html.haml b/app/views/admin/email_subscriptions/_compliance_settings.html.haml new file mode 100644 index 0000000000..3e951f2a4f --- /dev/null +++ b/app/views/admin/email_subscriptions/_compliance_settings.html.haml @@ -0,0 +1,22 @@ +.subheading-with-action + .subheading-with-action__subheading + %h3= t('.title') + %p= t('.lead') + +.actions-list + .actions-list__item + .actions-list__item__label + .actions-list__item__label__label + = t('.privacy_policy.title') + .actions-list__item__label__hint + = t('.privacy_policy.hint') + .actions-list__item__action + = link_to t('.privacy_policy.action'), admin_settings_about_path, class: 'button button-secondary' + .actions-list__item + .actions-list__item__label + .actions-list__item__label__label + = t('.additional_footer_text.title') + .actions-list__item__label__hint + = t('.additional_footer_text.hint') + .actions-list__item__action + = link_to t('.additional_footer_text.action'), admin_email_subscriptions_additional_footer_text_path, class: 'button button-secondary' diff --git a/app/views/admin/email_subscriptions/_danger_zone.html.haml b/app/views/admin/email_subscriptions/_danger_zone.html.haml new file mode 100644 index 0000000000..9bae92ca2e --- /dev/null +++ b/app/views/admin/email_subscriptions/_danger_zone.html.haml @@ -0,0 +1,22 @@ +.subheading-with-action + .subheading-with-action__subheading + %h3= t('.title') + +.actions-list + .actions-list__item + .actions-list__item__label + .actions-list__item__label__label + = t('.disable_feature.title') + .actions-list__item__label__hint + = t('.disable_feature.hint') + .actions-list__item__action + = link_to t('.disable_feature.action'), disable_admin_email_subscriptions_path, class: 'button button-secondary button--destructive', data: { method: 'post', confirm: t('admin.accounts.are_you_sure') } + - unless accounts.empty? + .actions-list__item + .actions-list__item__label + .actions-list__item__label__label + = t('.erase_all_data.title') + .actions-list__item__label__hint + = t('.erase_all_data.hint') + .actions-list__item__action + = link_to t('.erase_all_data.action'), purge_admin_email_subscriptions_path, class: 'button button-secondary button--destructive', data: { method: 'post', confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/admin/email_subscriptions/_roles.html.haml b/app/views/admin/email_subscriptions/_roles.html.haml new file mode 100644 index 0000000000..42c77ea8e3 --- /dev/null +++ b/app/views/admin/email_subscriptions/_roles.html.haml @@ -0,0 +1,36 @@ +.subheading-with-action + .subheading-with-action__subheading + %h3= t('.title') + %p= t('.lead') + - unless roles.empty? + .subheading-with-action__action + = link_to t('.manage_roles'), admin_roles_path, class: 'button button-secondary' + +.table-wrapper + - if roles.empty? + .empty-state + = emptyphaunt + + .empty-state__title-and-description + .empty-state__title-and-description__title + = t('.empty.no_roles_added') + .empty-state__title-and-description__description + = t('.empty.hint') + .empty-state__action + = link_to t('.manage_roles'), admin_roles_path, class: 'button' + - else + %table.table + %thead + %tr + %th= t('.role_name') + %th= t('.accounts') + %th + %tbody + - roles.each do |role| + %tr + %td + = role.name + %td + = role.users.count + %td.align-end + = link_to material_symbol('edit'), edit_admin_role_path(role), title: t('.edit_role'), class: 'table-icon-link' if can?(:update, role) diff --git a/app/views/admin/email_subscriptions/additional_footer_texts/show.html.haml b/app/views/admin/email_subscriptions/additional_footer_texts/show.html.haml new file mode 100644 index 0000000000..f6dadffbb6 --- /dev/null +++ b/app/views/admin/email_subscriptions/additional_footer_texts/show.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @admin_settings, url: admin_email_subscriptions_additional_footer_text_path do |form| + = render 'shared/error_messages', object: @admin_settings + + .fields-group + = form.input :email_footer_text, + as: :text, + input_html: { rows: 8 }, + wrapper: :with_block_label + + .actions + = form.button :button, + t('generic.save_changes'), + type: :submit diff --git a/app/views/admin/email_subscriptions/index.html.haml b/app/views/admin/email_subscriptions/index.html.haml new file mode 100644 index 0000000000..1f9e185181 --- /dev/null +++ b/app/views/admin/email_subscriptions/index.html.haml @@ -0,0 +1,28 @@ +- content_for :page_title do + = t('.title') + +- content_for :heading do + .heading-with-lead + %h1= t('.title') + %p.lead= t('.lead') + +%hr.spacer/ + +- if @enabled + = render 'roles', roles: @roles + %hr.spacer/ + = render 'accounts', accounts: @accounts + %hr.spacer/ + = render 'compliance_settings' + %hr.spacer/ + = render 'danger_zone', accounts: @accounts +- else + .simple_form + %p.lead= t('.disabled.description') + + - if Rails.application.config.x.email_subscriptions + = link_to t('.disabled.get_started'), admin_email_subscriptions_setup_path, class: 'button' + - else + %aside.callout + .content + .body= t('.disabled.cannot_be_enabled') diff --git a/app/views/admin/email_subscriptions/setups/show.html.haml b/app/views/admin/email_subscriptions/setups/show.html.haml new file mode 100644 index 0000000000..02fbc5d057 --- /dev/null +++ b/app/views/admin/email_subscriptions/setups/show.html.haml @@ -0,0 +1,25 @@ +- content_for :page_title do + = t('admin.email_subscriptions.index.title') + +- content_for :heading do + .heading-with-lead + %h1= t('admin.email_subscriptions.index.title') + %p.lead= t('admin.email_subscriptions.index.lead') + += simple_form_for @form, url: admin_email_subscriptions_setup_path, method: :post do |f| + = render 'shared/error_messages', object: @form + + %h3= t('.important_information') + + %ol.numbered-list + %li= t('.list.1_permission_explanation') + %li= t('.list.2_feature_explanation') + %li= t('.list.3_privacy_policy_warning') + %li= t('.list.4_cost_warning') + + .fields-group + = f.input :agreement_email_volume, as: :boolean, wrapper: :with_label + = f.input :agreement_privacy_and_terms, as: :boolean, wrapper: :with_label + + .actions + = f.button :button, t('.enable_feature'), type: :submit diff --git a/app/views/email_subscription_mailer/confirmation.html.haml b/app/views/email_subscription_mailer/confirmation.html.haml index 7d14d9ff59..51f414349e 100644 --- a/app/views/email_subscription_mailer/confirmation.html.haml +++ b/app/views/email_subscription_mailer/confirmation.html.haml @@ -18,3 +18,5 @@ - content_for :footer do %p.email-footer-p= t('email_subscription_mailer.notification.footer.reason_for_email_html', name: display_name(@subscription.account), unsubscribe_path: @unsubscribe_url) %p.email-footer-p= t('email_subscription_mailer.notification.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_path) + - if Setting.email_footer_text.present? + %p.email-footer-p= Setting.email_footer_text diff --git a/app/views/email_subscription_mailer/notification.html.haml b/app/views/email_subscription_mailer/notification.html.haml index 12848c59b3..0ee2aa2e0b 100644 --- a/app/views/email_subscription_mailer/notification.html.haml +++ b/app/views/email_subscription_mailer/notification.html.haml @@ -34,3 +34,5 @@ - content_for :footer do %p.email-footer-p= t('.footer.reason_for_email_html', name: display_name(@subscription.account), unsubscribe_path: @unsubscribe_url) %p.email-footer-p= t('.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_path) + - if Setting.email_footer_text.present? + %p.email-footer-p= Setting.email_footer_text diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml index 47880f1f6c..67026a62e8 100644 --- a/app/views/settings/privacy/show.html.haml +++ b/app/views/settings/privacy/show.html.haml @@ -38,7 +38,7 @@ .fields-group = ff.input :show_application, wrapper: :with_label - - if Mastodon::Feature.email_subscriptions_enabled? && current_user.can?(:manage_email_subscriptions) + - if Rails.application.config.x.email_subscriptions && Setting.email_subscriptions && current_user.can?(:manage_email_subscriptions) %h2= t('privacy.email_subscriptions') %p.lead= t('privacy.email_subscriptions_hint_html') diff --git a/app/workers/admin/email_subscriptions_purge_worker.rb b/app/workers/admin/email_subscriptions_purge_worker.rb new file mode 100644 index 0000000000..2a5d7c900c --- /dev/null +++ b/app/workers/admin/email_subscriptions_purge_worker.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Admin::EmailSubscriptionsPurgeWorker + include Sidekiq::Worker + + sidekiq_options lock: :until_executed, lock_ttl: 1.week.to_i + + def perform + EmailSubscription.in_batches.delete_all + end +end diff --git a/app/workers/email_distribution_worker.rb b/app/workers/email_distribution_worker.rb index 41edcb932c..d510b9d977 100644 --- a/app/workers/email_distribution_worker.rb +++ b/app/workers/email_distribution_worker.rb @@ -7,7 +7,7 @@ class EmailDistributionWorker sidekiq_options lock: :until_executed, lock_ttl: 1.day.to_i def perform(account_id) - return unless Mastodon::Feature.email_subscriptions_enabled? + return unless Rails.application.config.x.email_subscriptions && Setting.email_subscriptions @account = Account.find(account_id) diff --git a/config/initializers/email_subscriptions.rb b/config/initializers/email_subscriptions.rb new file mode 100644 index 0000000000..50608946e4 --- /dev/null +++ b/config/initializers/email_subscriptions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.x.email_subscriptions = ENV['DISABLE_EMAIL_SUBSCRIPTIONS'] != 'true' +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 176017f532..5d3344f0e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -487,6 +487,71 @@ en: resolved_dns_records_hint_html: The domain name resolves to the following MX domains, which are ultimately responsible for accepting email. Blocking an MX domain will block sign-ups from any email address which uses the same MX domain, even if the visible domain name is different. Be careful not to block major email providers. resolved_through_html: Resolved through %{domain} title: Blocked email domains + email_subscriptions: + accounts: + account: Account + active: Active + empty: + hint: No accounts have subscribers yet. + no_lists_yet: No lists yet + inactive: Inactive + last_email: Last email + lead: Accounts who have enabled the feature and have subscribers will show below. + status: Status + subscribers: Subscribers + title: Mailing lists + additional_footer_texts: + show: + title: Additional footer text + compliance_settings: + additional_footer_text: + action: Manage + hint: Optional text that appears in the footer of newsletter emails only + title: Additional footer text + lead: Email newsletters may be considered marketing emails, depending on the jurisdictions where you operate. + privacy_policy: + action: Manage + hint: This policy is linked in the footer of every email + title: Privacy policy + title: Compliance settings + danger_zone: + disable_feature: + action: Disable + hint: Turn off feature for all accounts + title: Disable feature + erase_all_data: + action: Erase data + hint: Permanently deletes all emails in all mailing lists + title: Erase all data + title: Danger zone + disabled_msg: Email subscriptions have been successfully disabled. + index: + disabled: + cannot_be_enabled: Your technical provider has not enabled this feature for your server. + description: This feature allows specified accounts to add a widget to their profiles, enabling visitors without a Mastodon account to receive their posts via email. + get_started: Get started + lead: Allow visitors to receive posts via email from dedicated accounts on this server. + title: Email newsletters + purged_msg: All email subscriptions data is being erased. + roles: + accounts: Accounts + edit_role: Edit role + empty: + hint: No one has permission to use this feature. + no_roles_added: No roles added + lead: Accounts with the following roles can enable this feature on their profiles. + manage_roles: Manage roles + role_name: Role name + title: Roles + setups: + show: + enable_feature: Enable feature + important_information: Important information + list: + 1_permission_explanation: When this feature is enabled, accounts with the designated permissions can add an email collection form to their profiles. + 2_feature_explanation: When visitors sign up on an account’s profile page and confirm their subscription, they’ll begin to receive email updates when the account creates new public posts. + 3_privacy_policy_warning: Server admins will have access to PII (email addresses) collected. As such, the privacy policy and Terms of Service for the server must be updated before using this feature. + 4_cost_warning: Emails may incur a fee depending on hosting setup. Discuss with your hosting provider before enabling, as this feature could drastically increase the amount of emails sent from your server. export_domain_allows: new: title: Import domain allows @@ -792,7 +857,7 @@ en: manage_custom_emojis: Manage Custom Emojis manage_custom_emojis_description: Allows users to manage custom emojis on the server manage_email_subscriptions: Manage Email Subscriptions - manage_email_subscriptions_description: Allow users to subscribe to users with this permission by email + manage_email_subscriptions_description: Allow users with this permission to enable the email newsletter feature for their account manage_federation: Manage Federation manage_federation_description: Allows users to block or allow federation with other domains, and control deliverability manage_invites: Manage Invites diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7bfc998249..c3fe3871b1 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -92,6 +92,7 @@ en: closed_registrations_message: Displayed when sign-ups are closed content_cache_retention_period: All posts from other servers (including boosts and replies) will be deleted after the specified number of days, without regard to any local user interaction with those posts. This includes posts where a local user has marked it as bookmarks or favorites. Private mentions between users from different instances will also be lost and impossible to restore. Use of this setting is intended for special purpose instances and breaks many user expectations when implemented for general purpose use. custom_css: You can apply custom styles on the web version of Mastodon. + email_footer_text: Optional text that appears in the footer of newsletter emails only. favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. landing_page: Selects what page new visitors see when they first arrive on your server. If you select "Trends", then trends needs to be enabled in the Discovery Settings. If you select "Local feed", then "Access to live feeds featuring local posts" needs to be set to "Everyone" in the Discovery Settings. mascot: Overrides the illustration in the advanced web interface. @@ -294,6 +295,7 @@ en: closed_registrations_message: Custom message when sign-ups are not available content_cache_retention_period: Remote content retention period custom_css: Custom CSS + email_footer_text: Additional footer text favicon: Favicon landing_page: Landing page for new visitors local_live_feed_access: Access to live feeds featuring local posts @@ -322,6 +324,9 @@ en: trendable_by_default: Allow trends without prior review trends: Enable trends wrapstodon: Enable Wrapstodon + form_email_subscriptions_confirmation: + agreement_email_volume: I understand that enabling this feature may significantly increase the volume of emails sent from the server and that I am solely responsible for any costs incurred. + agreement_privacy_and_terms: I have updated the Privacy Policy and Terms of Service. interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/navigation.rb b/config/navigation.rb index 66fdfdfe58..9ace5d5158 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -74,6 +74,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :warning_presets, safe_join([material_symbol('warning'), t('admin.warning_presets.title')]), admin_warning_presets_path, highlights_on: %r{/admin/warning_presets}, if: -> { current_user.can?(:manage_settings) } s.item :roles, safe_join([material_symbol('contact_mail'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) } s.item :announcements, safe_join([material_symbol('campaign'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) } + s.item :email_subscriptions, safe_join([material_symbol('mail'), t('admin.email_subscriptions.index.title')]), admin_email_subscriptions_path, highlights_on: %r{/admin/email_subscriptions}, if: -> { current_user.can?(:manage_settings) } s.item :custom_emojis, safe_join([material_symbol('mood'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) } s.item :webhooks, safe_join([material_symbol('inbox'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) } s.item :fasp, safe_join([material_symbol('extension'), t('admin.fasp.title')]), admin_fasp_providers_path, highlights_on: %r{/admin/fasp}, if: -> { current_user.can?(:manage_federation) } if Mastodon::Feature.fasp_enabled? diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 110f02bfdb..770a9cd777 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -26,6 +26,18 @@ namespace :admin do resources :email_domain_blocks, only: [:index, :new, :create], concerns: :batch + resources :email_subscriptions, only: [:index] do + collection do + post :purge + post :disable + end + end + + namespace :email_subscriptions do + resource :setup, only: [:show, :create] + resource :additional_footer_text, only: [:show, :update] + end + resources :action_logs, only: [:index] resources :warning_presets, except: [:new, :show] diff --git a/spec/requests/api/v1/accounts/email_subscriptions_spec.rb b/spec/requests/api/v1/accounts/email_subscriptions_spec.rb index ef7a31476a..313f7480e3 100644 --- a/spec/requests/api/v1/accounts/email_subscriptions_spec.rb +++ b/spec/requests/api/v1/accounts/email_subscriptions_spec.rb @@ -2,9 +2,13 @@ require 'rails_helper' -RSpec.describe 'Accounts Email Subscriptions API', feature: :email_subscriptions do +RSpec.describe 'Accounts Email Subscriptions API' do let(:account) { Fabricate(:user).account } + before do + Setting.email_subscriptions = true + end + describe 'POST /api/v1/accounts/:id/email_subscriptions' do context 'when the account has the permission' do let(:role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:manage_email_subscriptions]) }