Files
openproject/app/mailers/application_mailer.rb
Kabiru Mwenja 499d7820a2 Add render_mode flag and MailFormattingHelper
`format_text` accepts `render_mode:` (`:in_app_html`, `:external_html`,
`:external_text`), which resolves the `only_path`, `static_html` and
`plain_text` context flags as a set. External surfaces (mailer HTML
body, future RSS/PDF/webhook) need absolute URLs and static rendering
together; pinning the trio at the public API keeps callers from
forgetting one. Explicit primitive kwargs still override.

`MailFormattingHelper` exposes `format_mail_html` and `format_mail_text`
thin wrappers around `format_text(render_mode:)`. The `_html` / `_text`
suffix matches the `.html.erb` / `.text.erb` template extension so
caller intent stays visible in the view, with no introspection of
`formats`.

The five WorkPackageMailer view sites use the helpers; `_work_package_details`,
`mentioned.html`, `mentioned.text`, `watcher_changed.html`, `watcher_changed.text`
drop the `static_html:`/`only_path:`/`plain_text:` boilerplate.
2026-05-27 13:04:26 +03:00

217 lines
6.8 KiB
Ruby

# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class ApplicationMailer < ActionMailer::Base
layout "mailer"
helper :application, # for format_text
:work_packages, # for css classes
:custom_fields, # for show_value
:mail_formatting, # for format_mail_html / format_mail_text
:mail_layout # for layouting
include OpenProject::LocaleHelper
# Send all delayed mails with the following job
self.delivery_job = ::Mails::MailerJob
# wrap in a lambda to allow changing at run-time
default from: Proc.new { Setting.mail_from }
class << self
##
# Provide an easy way to get the default from address
# which is overridden for SaaS for tenant specific from addresses
#
# @return [String] the default from address
def mail_from
default[:from].call
end
##
# Provide an easy way to get the default reply_to address
# which is overridden for SaaS for tenant specific from addresses
#
# @return [String] the default from address
def reply_to
if default[:reply_to]
default[:reply_to].call
else
mail_from
end
end
##
# Return the email address of reply to
#
# @return [String] the default from address
def reply_to_address
address = reply_to
# Extract email from "Name <email>" format if present
address[/<(.+)>/, 1] || address
end
def host
if OpenProject::Configuration.rails_relative_url_root.blank?
Setting.host_name
else
Setting.host_name.to_s.gsub(%r{/.*\z}, "")
end
end
delegate :protocol, to: :Setting
def default_url_options
options = super.merge(host:, protocol:)
if OpenProject::Configuration.rails_relative_url_root.present?
options[:script_name] = OpenProject::Configuration.rails_relative_url_root
end
options
end
end
# Sets a Message-ID header.
#
# While the value is set in here, email gateways such as postmark, unless instructed explicitly will assign
# their own message id overwriting the value calculated in here.
#
# Because of this, the message id and the value affected by it (In-Reply-To) is not relied upon when an email response
# is handled by OpenProject.
def message_id(object, user)
headers["Message-ID"] = "<#{message_id_value(object, user)}>"
end
# Sets a References header.
#
# The value is used within the MailHandler to find the appropriate objects for update
# when a mail has been received but should also allow mail clients to mails
# by the referenced entities. Because of this it might make sense to provide more than one object
# of reference. E.g. for a message, the message parent can also be provided.
def references(*objects)
refs = objects.map do |object|
if object.is_a?(Journal)
"<#{references_value(object.journable)}> <#{references_value(object)}>"
else
"<#{references_value(object)}>"
end
end
headers["References"] = refs.join(" ")
end
# Prepends given fields with 'X-OpenProject-' to save some duplication
def open_project_headers(hash)
hash.each { |key, value| headers["X-OpenProject-#{key}"] = value.to_s }
end
private
def default_formats_for_setting(format)
format.html unless Setting.plain_text_mail?
format.text
end
##
# Overwrite mailer method to prevent sending mails to locked users.
# Usually this would accept a string for the `to:` argument, but
# we always require an actual user object since fed95796.
def mail(headers = {}, &block)
block ||= method(:default_formats_for_setting)
to = headers[:to]
if to
raise ArgumentError, "Recipient needs to be instance of User" unless to.is_a?(User)
if to.locked? || to.deleted?
Rails.logger.info "Not sending #{action_name} mail to locked user #{to.id} (#{to.login})"
return
end
end
super(headers.merge(to: to.mail), &block)
end
def send_localized_mail(user, delivery_method_options: {})
with_locale_for(user) do
subject = yield
mail to: user, subject:, delivery_method_options:
end
end
# Generates a unique value for the Message-ID header.
# Contains:
# * an 'op' prefix
# * an object id part that consists of the object's class name and the id unless that part is provided as a string
# * the current time
# * the recipient's id
#
# Note that this values, as opposed to the one from #references_value is unique.
def message_id_value(object, recipient)
object_reference = case object
when String
object
else
"#{object.class.name.demodulize.underscore}-#{object.id}"
end
hash = "op" \
"." \
"#{object_reference}" \
"." \
"#{Time.current.strftime('%Y%m%d%H%M%S')}" \
"." \
"#{recipient.id}"
"#{hash}@#{header_host_value}"
end
# Generates a value for the References header.
# Contains:
# * an 'op' prefix
# * an object id part that consists of the object's class name and the id
#
# Note that this values, as opposed to the one from #message_id_value is not unique.
# It in fact is aimed not not so that similar messages (i.e. those belonging to the same
# work package and journal) end up being grouped together.
def references_value(object)
hash = "op" \
"." \
"#{object.class.name.demodulize.underscore}-#{object.id}"
"#{hash}@#{header_host_value}"
end
def header_host_value
host = ApplicationMailer.mail_from.to_s.gsub(%r{\A.*@}, "")
host = "#{::Socket.gethostname}.openproject" if host.empty?
host
end
end