123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Transaction::Notification
- include ChecksHumanChanges
- # Following SMTP error codes will be handled gracefully.
- # They will be logged at info level only and the code will not propagate up the error.
- # Other SMTP error codes will stop processing and exit with logging it at error level.
- #
- # 4xx - temporary issues.
- # 52x - permanent receiving server errors.
- # 55x - permanent receiving mailbox errors.
- SILENCABLE_SMTP_ERROR_CODES = [400..499, 520..529, 550..559].freeze
- =begin
- {
- object: 'Ticket',
- type: 'update',
- object_id: 123,
- interface_handle: 'application_server', # application_server|websocket|scheduler
- changes: {
- 'attribute1' => [before, now],
- 'attribute2' => [before, now],
- },
- created_at: Time.zone.now,
- user_id: 123,
- },
- =end
- attr_accessor :recipients_and_channels, :recipients_reason
- def initialize(item, params = {})
- @item = item
- @params = params
- @recipients_and_channels = []
- @recipients_reason = {}
- end
- def ticket
- @ticket ||= Ticket.find_by(id: @item[:object_id])
- end
- def article_by_item
- return if !@item[:article_id]
- article = Ticket::Article.find(@item[:article_id])
- # ignore notifications
- sender = Ticket::Article::Sender.lookup(id: article.sender_id)
- if sender&.name == 'System'
- return if @item[:changes].blank? && article.preferences[:notification] != true
- if article.preferences[:notification] != true
- article = nil
- end
- end
- article
- end
- def article
- @article ||= article_by_item
- end
- def current_user
- @current_user ||= User.lookup(id: @item[:user_id]) || User.lookup(id: 1)
- end
- def perform
- # return if we run import mode
- return if Setting.get('import_mode')
- return if %w[Ticket Ticket::Article].exclude?(@item[:object])
- return if @params[:disable_notification]
- return if !ticket
- prepare_recipients_and_reasons
- # send notifications
- recipients_and_channels.each do |recipient_settings|
- send_to_single_recipient(recipient_settings)
- end
- end
- def prepare_recipients_and_reasons
- # loop through all group users
- possible_recipients = possible_recipients_of_group(ticket.group_id)
- # loop through all mention users
- mention_users = Mention.where(mentionable_type: @item[:object], mentionable_id: @item[:object_id]).map(&:user)
- if mention_users.present?
- # only notify if read permission on group are given
- mention_users.each do |mention_user|
- next if !mention_user.group_access?(ticket.group_id, 'read')
- possible_recipients.push mention_user
- @recipients_reason[mention_user.id] = __('You are receiving this because you were mentioned in this ticket.')
- end
- end
- # apply owner
- if ticket.owner_id != 1
- possible_recipients.push ticket.owner
- @recipients_reason[ticket.owner_id] = __('You are receiving this because you are the owner of this ticket.')
- end
- # apply out of office agents
- possible_recipients_additions = Set.new
- possible_recipients.each do |user|
- ooo_replacements(
- user: user,
- replacements: possible_recipients_additions,
- reasons: recipients_reason,
- ticket: ticket,
- )
- end
- if possible_recipients_additions.present?
- # join unique entries
- possible_recipients |= possible_recipients_additions.to_a
- end
- recipients_reason_by_notifications_settings(possible_recipients)
- end
- def recipients_reason_by_notifications_settings(possible_recipients)
- already_checked_recipient_ids = {}
- possible_recipients.each do |user|
- result = NotificationFactory::Mailer.notification_settings(user, ticket, @item[:type])
- next if !result
- next if already_checked_recipient_ids[user.id]
- already_checked_recipient_ids[user.id] = true
- @recipients_and_channels.push result
- next if recipients_reason[user.id]
- @recipients_reason[user.id] = __('You are receiving this because you are a member of the group of this ticket.')
- end
- end
- def send_to_single_recipient(recipient_settings)
- user = recipient_settings[:user]
- channels = recipient_settings[:channels]
- # ignore user who changed it by him self via web
- if @params[:interface_handle] == 'application_server'
- return if article&.updated_by_id == user.id
- return if !article && @item[:user_id] == user.id
- end
- # ignore inactive users
- return if !user.active?
- blocked_in_days = user.mail_delivery_failed_blocked_days
- if blocked_in_days.positive?
- Rails.logger.info "Send no system notifications to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
- return
- end
- # ignore if no changes has been done
- changes = human_changes(@item[:changes], ticket, user)
- return if @item[:type] == 'update' && !article && changes.blank?
- # check if today already notified
- if @item[:type] == 'reminder_reached' || @item[:type] == 'escalation' || @item[:type] == 'escalation_warning'
- identifier = user.email
- if !identifier || identifier == ''
- identifier = user.login
- end
- already_notified_cutoff = Time.use_zone(Setting.get('timezone_default')) { Time.current.beginning_of_day }
- already_notified = History.where(
- history_type_id: History.type_lookup('notification').id,
- history_object_id: History.object_lookup('Ticket').id,
- o_id: ticket.id
- ).where('created_at > ?', already_notified_cutoff).exists?(['value_to LIKE ?', "%#{SqlHelper.quote_like(identifier)}(#{SqlHelper.quote_like(@item[:type])}:%"])
- return if already_notified
- end
- # create online notification
- used_channels = []
- if channels['online']
- used_channels.push 'online'
- created_by_id = @item[:user_id] || 1
- # delete old notifications
- if @item[:type] == 'reminder_reached'
- seen = false
- created_by_id = 1
- OnlineNotification.remove_by_type('Ticket', ticket.id, @item[:type], user)
- elsif @item[:type] == 'escalation' || @item[:type] == 'escalation_warning'
- seen = false
- created_by_id = 1
- OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation', user)
- OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation_warning', user)
- # on updates without state changes create unseen messages
- elsif @item[:type] != 'create' && (@item[:changes].blank? || @item[:changes]['state_id'].blank?)
- seen = false
- else
- seen = OnlineNotification.seen_state?(ticket, user.id)
- end
- OnlineNotification.add(
- type: @item[:type],
- object: @item[:object],
- o_id: @item[:object].eql?('Ticket') ? ticket.id : article.id,
- seen: seen,
- created_by_id: created_by_id,
- user_id: user.id,
- )
- Rails.logger.debug { "sent ticket online notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email})" }
- end
- # ignore email channel notification and empty emails
- if !channels['email'] || user.email.blank?
- add_recipient_list_to_history(ticket, user, used_channels, @item[:type])
- return
- end
- used_channels.push 'email'
- add_recipient_list_to_history(ticket, user, used_channels, @item[:type])
- # get user based notification template
- # if create, send create message / block update messages
- template = case @item[:type]
- when 'create'
- 'ticket_create'
- when 'update'
- 'ticket_update'
- when 'reminder_reached'
- 'ticket_reminder_reached'
- when 'escalation'
- 'ticket_escalation'
- when 'escalation_warning'
- 'ticket_escalation_warning'
- when 'update.merged_into'
- 'ticket_update_merged_into'
- when 'update.received_merge'
- 'ticket_update_received_merge'
- when 'update.reaction'
- 'ticket_article_update_reaction'
- else
- raise "unknown type for notification #{@item[:type]}"
- end
- attachments = []
- if article
- attachments = article.attachments_inline
- end
- NotificationFactory::Mailer.notification(
- template: template,
- user: user,
- objects: {
- ticket: ticket,
- article: article,
- recipient: user,
- current_user: current_user,
- changes: changes,
- reason: recipients_reason[user.id],
- },
- message_id: "<notification.#{DateTime.current.to_fs(:number)}.#{ticket.id}.#{user.id}.#{SecureRandom.uuid}@#{Setting.get('fqdn')}>",
- references: ticket.get_references,
- main_object: ticket,
- attachments: attachments,
- )
- Rails.logger.debug { "sent ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email})" }
- rescue Channel::DeliveryError => e
- status_code = begin
- e.original_error.response.status.to_i
- rescue
- raise e
- end
- if SILENCABLE_SMTP_ERROR_CODES.any? { |elem| elem.include? status_code }
- Rails.logger.info do
- "could not send ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email}) #{e.original_error}"
- end
- return
- end
- raise e
- end
- def add_recipient_list_to_history(ticket, user, channels, type)
- return if channels.blank?
- identifier = user.email.presence || user.login
- recipient_list = "#{identifier}(#{type}:#{channels.join(',')})"
- History.add(
- o_id: ticket.id,
- history_type: 'notification',
- history_object: 'Ticket',
- value_to: recipient_list,
- created_by_id: @item[:user_id] || 1
- )
- end
- private
- def ooo_replacements(user:, replacements:, ticket:, reasons:)
- replacement = user.out_of_office_agent
- return if !replacement
- return if !TicketPolicy.new(replacement, ticket).agent_read_access?
- return if !replacements.add?(replacement)
- reasons[replacement.id] = __('You are receiving this because you are out-of-office replacement for a participant of this ticket.')
- end
- def possible_recipients_of_group(group_id)
- Rails.cache.fetch("User/notification/possible_recipients_of_group/#{group_id}/#{User.latest_change}", expires_in: 20.seconds) do
- User.group_access(group_id, 'full').sort_by(&:login)
- end
- end
- end
|