|
|
|
#-- encoding: UTF-8
|
|
|
|
#-- copyright
|
|
|
|
# OpenProject is a project management system.
|
|
|
|
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
|
|
|
|
#
|
|
|
|
# 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 doc/COPYRIGHT.rdoc for more details.
|
|
|
|
#++
|
|
|
|
|
|
|
|
class UserMailer < ActionMailer::Base
|
|
|
|
helper :application, # for format_text
|
|
|
|
:work_packages, # for css classes
|
|
|
|
:custom_fields # for show_value
|
|
|
|
|
|
|
|
include OpenProject::LocaleHelper
|
|
|
|
|
|
|
|
# wrap in a lambda to allow changing at run-time
|
|
|
|
default from: Proc.new { Setting.mail_from }
|
|
|
|
|
|
|
|
def test_mail(user)
|
|
|
|
@welcome_url = url_for(controller: '/welcome')
|
|
|
|
|
|
|
|
headers['X-OpenProject-Type'] = 'Test'
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
mail to: "\"#{user.name}\" <#{user.mail}>", subject: 'OpenProject Test'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def work_package_added(user, issue)
|
|
|
|
@issue = issue
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @issue.project.identifier,
|
|
|
|
'Issue-Id' => @issue.id,
|
|
|
|
'Issue-Author' => @issue.author.login,
|
|
|
|
'Type' => 'WorkPackage'
|
|
|
|
open_project_headers 'Issue-Assignee' => @issue.assigned_to.login if @issue.assigned_to
|
|
|
|
|
|
|
|
message_id @issue, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "[#{@issue.project.name} - #{ @issue }]"
|
|
|
|
subject << " (#{@issue.status.name})" if @issue.status
|
|
|
|
subject << " #{@issue.subject}"
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def work_package_updated(user, journal, author = User.current)
|
|
|
|
# Delayed job does not preserve the closure of the job that is delayed.
|
|
|
|
# Thus, if the method is called within a delayed job, it does contain the
|
|
|
|
# default user (anonymous) and not the original user that called the method.
|
|
|
|
#
|
|
|
|
# The mail interceptor 'RemoveSelfNotificationsInterceptor' assumes the
|
|
|
|
# original user to be available. Otherwise, it cannot fulfill its duty.
|
|
|
|
User.current = author if User.current != author
|
|
|
|
|
|
|
|
@journal = journal
|
|
|
|
@issue = journal.journable.reload
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @issue.project.identifier,
|
|
|
|
'Issue-Id' => @issue.id,
|
|
|
|
'Issue-Author' => @issue.author.login,
|
|
|
|
'Type' => 'WorkPackage'
|
|
|
|
open_project_headers 'Issue-Assignee' => @issue.assigned_to.login if @issue.assigned_to
|
|
|
|
|
|
|
|
message_id @journal, user
|
|
|
|
references @issue, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "[#{@issue.project.name} - #{@issue.type.name} ##{@issue.id}] "
|
|
|
|
subject << "(#{@issue.status.name}) " if @journal.details[:status_id]
|
|
|
|
subject << @issue.subject
|
|
|
|
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def password_lost(token)
|
|
|
|
return unless token.user # token's can have no user
|
|
|
|
|
|
|
|
@token = token
|
|
|
|
@reset_password_url = url_for(controller: '/account',
|
|
|
|
action: :lost_password,
|
|
|
|
token: @token.value)
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Account'
|
|
|
|
|
|
|
|
user = @token.user
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = t(:mail_subject_lost_password, value: Setting.app_title)
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def copy_project_failed(user, source_project, target_project_name)
|
|
|
|
@source_project = source_project
|
|
|
|
@target_project_name = target_project_name
|
|
|
|
|
|
|
|
open_project_headers 'Source-Project' => source_project.identifier,
|
|
|
|
'Author' => user.login
|
|
|
|
|
|
|
|
message_id source_project, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = I18n.t('copy_project.failed', source_project_name: source_project.name)
|
|
|
|
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def copy_project_succeeded(user, source_project, target_project, errors)
|
|
|
|
@source_project = source_project
|
|
|
|
@target_project = target_project
|
|
|
|
@errors = errors
|
|
|
|
|
|
|
|
open_project_headers 'Source-Project' => source_project.identifier,
|
|
|
|
'Target-Project' => target_project.identifier,
|
|
|
|
'Author' => user.login
|
|
|
|
|
|
|
|
message_id target_project, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = I18n.t('copy_project.succeeded', target_project_name: target_project.name)
|
|
|
|
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def news_added(user, news)
|
|
|
|
@news = news
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'News'
|
|
|
|
open_project_headers 'Project' => @news.project.identifier if @news.project
|
|
|
|
|
|
|
|
message_id @news, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "#{News.model_name.human}: #{@news.title}"
|
|
|
|
subject = "[#{@news.project.name}] #{subject}" if @news.project
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def user_signed_up(token)
|
|
|
|
return unless token.user
|
|
|
|
|
|
|
|
@token = token
|
|
|
|
@activation_url = url_for(controller: '/account',
|
|
|
|
action: :activate,
|
|
|
|
token: @token.value)
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Account'
|
|
|
|
|
|
|
|
user = token.user
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = t(:mail_subject_register, value: Setting.app_title)
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def news_comment_added(user, comment)
|
|
|
|
@comment = comment
|
|
|
|
@news = @comment.commented
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @news.project.identifier if @news.project
|
|
|
|
|
|
|
|
message_id @comment, user
|
|
|
|
references @news, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "#{News.model_name.human}: #{@news.title}"
|
|
|
|
subject = "Re: [#{@news.project.name}] #{subject}" if @news.project
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def wiki_content_added(user, wiki_content)
|
|
|
|
@wiki_content = wiki_content
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @wiki_content.project.identifier,
|
|
|
|
'Wiki-Page-Id' => @wiki_content.page.id,
|
|
|
|
'Type' => 'Wiki'
|
|
|
|
|
|
|
|
message_id @wiki_content, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "[#{@wiki_content.project.name}] #{t(:mail_subject_wiki_content_added, id: @wiki_content.page.pretty_title)}"
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def wiki_content_updated(user, wiki_content)
|
|
|
|
@wiki_content = wiki_content
|
|
|
|
@wiki_diff_url = url_for(controller: '/wiki',
|
|
|
|
action: :diff,
|
|
|
|
project_id: wiki_content.project,
|
|
|
|
id: wiki_content.page.title,
|
|
|
|
# using wiki_content.version + 1 because at this point the journal is not saved yet
|
|
|
|
version: wiki_content.version + 1)
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @wiki_content.project.identifier,
|
|
|
|
'Wiki-Page-Id' => @wiki_content.page.id,
|
|
|
|
'Type' => 'Wiki'
|
|
|
|
|
|
|
|
message_id @wiki_content, user
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "[#{@wiki_content.project.name}] #{t(:mail_subject_wiki_content_updated, id: @wiki_content.page.pretty_title)}"
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def message_posted(user, message)
|
|
|
|
@message = message
|
|
|
|
@message_url = topic_url(@message.root, r: @message.id, anchor: "message-#{@message.id}")
|
|
|
|
|
|
|
|
open_project_headers 'Project' => @message.project.identifier,
|
|
|
|
'Wiki-Page-Id' => @message.parent_id || @message.id,
|
|
|
|
'Type' => 'Forum'
|
|
|
|
|
|
|
|
message_id @message, user
|
|
|
|
references @message.parent, user if @message.parent
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = "[#{@message.board.project.name} - #{@message.board.name} - msg#{@message.root.id}] #{@message.subject}"
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def account_activated(user)
|
|
|
|
@user = user
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Account'
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = t(:mail_subject_register, value: Setting.app_title)
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def account_information(user, password)
|
|
|
|
@user = user
|
|
|
|
@password = password
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Account'
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = t(:mail_subject_register, value: Setting.app_title)
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def account_activation_requested(admin, user)
|
|
|
|
@user = user
|
|
|
|
@activation_url = url_for(controller: '/users',
|
|
|
|
action: :index,
|
|
|
|
status: User::STATUSES[:registered],
|
|
|
|
sort: 'created_at:desc')
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Account'
|
|
|
|
|
|
|
|
with_locale_for(admin) do
|
|
|
|
subject = t(:mail_subject_account_activation_request, value: Setting.app_title)
|
|
|
|
mail to: admin.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def reminder_mail(user, issues, days)
|
|
|
|
@issues = issues
|
|
|
|
@days = days
|
|
|
|
|
|
|
|
@assigned_issues_url = url_for(controller: :work_packages,
|
|
|
|
action: :index,
|
|
|
|
set_filter: 1,
|
|
|
|
assigned_to_id: user.id,
|
|
|
|
sort: 'due_date:asc')
|
|
|
|
|
|
|
|
open_project_headers 'Type' => 'Issue'
|
|
|
|
|
|
|
|
with_locale_for(user) do
|
|
|
|
subject = t(:mail_subject_reminder, count: @issues.size, days: @days)
|
|
|
|
mail to: user.mail, subject: subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Activates/deactivates email deliveries during +block+
|
|
|
|
def self.with_deliveries(temporary_state = true, &_block)
|
|
|
|
old_state = ActionMailer::Base.perform_deliveries
|
|
|
|
ActionMailer::Base.perform_deliveries = temporary_state
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
ActionMailer::Base.perform_deliveries = old_state
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.generate_message_id(object, user)
|
|
|
|
# id + timestamp should reduce the odds of a collision
|
|
|
|
# as far as we don't send multiple emails for the same object
|
|
|
|
journable = (object.is_a? Journal) ? object.journable : object
|
|
|
|
|
|
|
|
timestamp = mail_timestamp(object)
|
|
|
|
hash = 'openproject'\
|
|
|
|
'.'\
|
|
|
|
"#{journable.class.name.demodulize.underscore}"\
|
|
|
|
'-'\
|
|
|
|
"#{user.id}"\
|
|
|
|
'-'\
|
|
|
|
"#{journable.id}"\
|
|
|
|
'.'\
|
|
|
|
"#{timestamp.strftime('%Y%m%d%H%M%S')}"
|
|
|
|
host = Setting.mail_from.to_s.gsub(%r{\A.*@}, '')
|
|
|
|
host = "#{::Socket.gethostname}.openproject" if host.empty?
|
|
|
|
"#{hash}@#{host}"
|
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
|
|
|
# Option 1 to take out an html part: Leave the part out
|
|
|
|
# while creating the mail. Since rails internally uses three
|
|
|
|
# different ways to create a mail (passing a block, giving parameters
|
|
|
|
# with optional template, or passing the body directly), we would have
|
|
|
|
# to replicate a lot of rails code to modify all three ways.
|
|
|
|
# Therefore, we use option 2: modifying the set of parts rails
|
|
|
|
# created internally as a result of the above ways, as this is
|
|
|
|
# much shorter.
|
|
|
|
# On the downside, this might break if ActionMailer changes the signature
|
|
|
|
# or semantics of the following function. However, we should at least
|
|
|
|
# notice this as there are tests for checking the no-html setting.
|
|
|
|
def collect_responses_and_parts_order(headers)
|
|
|
|
responses, parts_order = super(headers)
|
|
|
|
if Setting.plain_text_mail?
|
|
|
|
responses.delete_if { |response| response[:content_type] == 'text/html' }
|
|
|
|
parts_order.delete_if { |part| part == 'text/html' } unless parts_order.nil?
|
|
|
|
end
|
|
|
|
[responses, parts_order]
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def self.mail_timestamp(object)
|
|
|
|
if object.respond_to? :created_at
|
|
|
|
timestamp = object.send(object.respond_to?(:created_at) ? :created_at : :updated_at)
|
|
|
|
else
|
|
|
|
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.host
|
|
|
|
if OpenProject::Configuration.rails_relative_url_root.blank?
|
|
|
|
Setting.host_name
|
|
|
|
else
|
|
|
|
Setting.host_name.to_s.gsub(%r{\/.*\z}, '')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.protocol
|
|
|
|
Setting.protocol
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.default_url_options
|
|
|
|
options = super.merge host: host, protocol: protocol
|
|
|
|
unless OpenProject::Configuration.rails_relative_url_root.blank?
|
|
|
|
options[:script_name] = OpenProject::Configuration.rails_relative_url_root
|
|
|
|
end
|
|
|
|
|
|
|
|
options
|
|
|
|
end
|
|
|
|
|
|
|
|
def message_id(object, user)
|
|
|
|
headers['Message-ID'] = "<#{self.class.generate_message_id(object, user)}>"
|
|
|
|
end
|
|
|
|
|
|
|
|
def references(object, user)
|
|
|
|
headers['References'] = "<#{self.class.generate_message_id(object, user)}>"
|
|
|
|
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
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Interceptors
|
|
|
|
#
|
|
|
|
# These are registered in config/initializers/register_mail_interceptors.rb
|
|
|
|
#
|
|
|
|
# Unfortunately, this results in changes on the interceptor classes during development mode
|
|
|
|
# not being reflected until a server restart.
|
|
|
|
|
|
|
|
class DefaultHeadersInterceptor
|
|
|
|
def self.delivering_email(mail)
|
|
|
|
mail.headers(default_headers)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.default_headers
|
|
|
|
{
|
|
|
|
'X-Mailer' => 'OpenProject',
|
|
|
|
'X-OpenProject-Host' => Setting.host_name,
|
|
|
|
'X-OpenProject-Site' => Setting.app_title,
|
|
|
|
'Precedence' => 'bulk',
|
|
|
|
'Auto-Submitted' => 'auto-generated'
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RemoveSelfNotificationsInterceptor
|
|
|
|
def self.delivering_email(mail)
|
|
|
|
user_mail = User.current.mail
|
|
|
|
# This may be called within a delayed job. Within a delayed job user
|
|
|
|
# preferences may not be loaded. Furthermore, some users don't have
|
|
|
|
# persisted preferences. Thus, we only load user preferences if preferences
|
|
|
|
# are available.
|
|
|
|
user_pref = User.current.pref.reload if User.current.pref.persisted?
|
|
|
|
|
|
|
|
if user_pref && user_pref[:no_self_notified]
|
|
|
|
mail.to = mail.to.reject { |address| address == user_mail } if mail.to.present?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class DoNotSendMailsWithoutReceiverInterceptor
|
|
|
|
def self.delivering_email(mail)
|
|
|
|
receivers = [mail.to, mail.cc, mail.bcc]
|
|
|
|
# the above fields might be empty arrays (if entries have been removed
|
|
|
|
# by another interceptor) or nil, therefore checking for blank?
|
|
|
|
mail.perform_deliveries = false if receivers.all?(&:blank?)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# helper object for `rake redmine:send_reminders`
|
|
|
|
|
|
|
|
class DueIssuesReminder
|
|
|
|
def initialize(days = nil, project_id = nil, type_id = nil, user_ids = [])
|
|
|
|
@days = days ? days.to_i : 7
|
|
|
|
@project = Project.find_by_id(project_id)
|
|
|
|
@type = Type.find_by_id(type_id)
|
|
|
|
@user_ids = user_ids
|
|
|
|
end
|
|
|
|
|
|
|
|
def remind_users
|
|
|
|
s = ARCondition.new ["#{Status.table_name}.is_closed = ? AND #{WorkPackage.table_name}.due_date <= ?", false, @days.days.from_now.to_date]
|
|
|
|
s << "#{WorkPackage.table_name}.assigned_to_id IS NOT NULL"
|
|
|
|
s << ["#{WorkPackage.table_name}.assigned_to_id IN (?)", @user_ids] if @user_ids.any?
|
|
|
|
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
|
|
|
|
s << "#{WorkPackage.table_name}.project_id = #{@project.id}" if @project
|
|
|
|
s << "#{WorkPackage.table_name}.type_id = #{@type.id}" if @type
|
|
|
|
|
|
|
|
issues_by_assignee = WorkPackage.find(:all, include: [:status, :assigned_to, :project, :type],
|
|
|
|
conditions: s.conditions
|
|
|
|
).group_by(&:assigned_to)
|
|
|
|
issues_by_assignee.each do |assignee, issues|
|
|
|
|
UserMailer.reminder_mail(assignee, issues, @days).deliver if assignee && assignee.active?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|