notification.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class Transaction::Notification
  3. include ChecksHumanChanges
  4. # Following SMTP error codes will be handled gracefully.
  5. # They will be logged at info level only and the code will not propagate up the error.
  6. # Other SMTP error codes will stop processing and exit with logging it at error level.
  7. #
  8. # 4xx - temporary issues.
  9. # 52x - permanent receiving server errors.
  10. # 55x - permanent receiving mailbox errors.
  11. SILENCABLE_SMTP_ERROR_CODES = [400..499, 520..529, 550..559].freeze
  12. =begin
  13. {
  14. object: 'Ticket',
  15. type: 'update',
  16. object_id: 123,
  17. interface_handle: 'application_server', # application_server|websocket|scheduler
  18. changes: {
  19. 'attribute1' => [before, now],
  20. 'attribute2' => [before, now],
  21. },
  22. created_at: Time.zone.now,
  23. user_id: 123,
  24. },
  25. =end
  26. attr_accessor :recipients_and_channels, :recipients_reason
  27. def initialize(item, params = {})
  28. @item = item
  29. @params = params
  30. @recipients_and_channels = []
  31. @recipients_reason = {}
  32. end
  33. def ticket
  34. @ticket ||= Ticket.find_by(id: @item[:object_id])
  35. end
  36. def article_by_item
  37. return if !@item[:article_id]
  38. article = Ticket::Article.find(@item[:article_id])
  39. # ignore notifications
  40. sender = Ticket::Article::Sender.lookup(id: article.sender_id)
  41. if sender&.name == 'System'
  42. return if @item[:changes].blank? && article.preferences[:notification] != true
  43. if article.preferences[:notification] != true
  44. article = nil
  45. end
  46. end
  47. article
  48. end
  49. def article
  50. @article ||= article_by_item
  51. end
  52. def current_user
  53. @current_user ||= User.lookup(id: @item[:user_id]) || User.lookup(id: 1)
  54. end
  55. def perform
  56. # return if we run import mode
  57. return if Setting.get('import_mode')
  58. return if %w[Ticket Ticket::Article].exclude?(@item[:object])
  59. return if @params[:disable_notification]
  60. return if !ticket
  61. prepare_recipients_and_reasons
  62. # send notifications
  63. recipients_and_channels.each do |recipient_settings|
  64. send_to_single_recipient(recipient_settings)
  65. end
  66. end
  67. def prepare_recipients_and_reasons
  68. # loop through all group users
  69. possible_recipients = possible_recipients_of_group(ticket.group_id)
  70. # loop through all mention users
  71. mention_users = Mention.where(mentionable_type: @item[:object], mentionable_id: @item[:object_id]).map(&:user)
  72. if mention_users.present?
  73. # only notify if read permission on group are given
  74. mention_users.each do |mention_user|
  75. next if !mention_user.group_access?(ticket.group_id, 'read')
  76. possible_recipients.push mention_user
  77. @recipients_reason[mention_user.id] = __('You are receiving this because you were mentioned in this ticket.')
  78. end
  79. end
  80. # apply owner
  81. if ticket.owner_id != 1
  82. possible_recipients.push ticket.owner
  83. @recipients_reason[ticket.owner_id] = __('You are receiving this because you are the owner of this ticket.')
  84. end
  85. # apply out of office agents
  86. possible_recipients_additions = Set.new
  87. possible_recipients.each do |user|
  88. ooo_replacements(
  89. user: user,
  90. replacements: possible_recipients_additions,
  91. reasons: recipients_reason,
  92. ticket: ticket,
  93. )
  94. end
  95. if possible_recipients_additions.present?
  96. # join unique entries
  97. possible_recipients |= possible_recipients_additions.to_a
  98. end
  99. recipients_reason_by_notifications_settings(possible_recipients)
  100. end
  101. def recipients_reason_by_notifications_settings(possible_recipients)
  102. already_checked_recipient_ids = {}
  103. possible_recipients.each do |user|
  104. result = NotificationFactory::Mailer.notification_settings(user, ticket, @item[:type])
  105. next if !result
  106. next if already_checked_recipient_ids[user.id]
  107. already_checked_recipient_ids[user.id] = true
  108. @recipients_and_channels.push result
  109. next if recipients_reason[user.id]
  110. @recipients_reason[user.id] = __('You are receiving this because you are a member of the group of this ticket.')
  111. end
  112. end
  113. def recipient_myself?(user)
  114. return false if @params[:interface_handle] != 'application_server'
  115. return true if article&.updated_by_id == user.id
  116. return true if !article && @item[:user_id] == user.id
  117. false
  118. end
  119. def send_to_single_recipient(recipient_settings)
  120. user = recipient_settings[:user]
  121. channels = recipient_settings[:channels]
  122. # ignore user who changed it by him self via web
  123. return if recipient_myself?(user)
  124. # ignore inactive users
  125. return if !user.active?
  126. blocked_in_days = user.mail_delivery_failed_blocked_days
  127. if blocked_in_days.positive?
  128. Rails.logger.info "Send no system notifications to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
  129. return
  130. end
  131. # ignore if no changes has been done
  132. changes = human_changes(@item[:changes], ticket, user)
  133. return if @item[:type] == 'update' && !article && changes.blank?
  134. # check if today already notified
  135. if %w[reminder_reached escalation escalation_warning].include?(@item[:type])
  136. identifier = user.email
  137. if !identifier || identifier == ''
  138. identifier = user.login
  139. end
  140. already_notified_cutoff = Time.use_zone(Setting.get('timezone_default')) { Time.current.beginning_of_day }
  141. already_notified = History.where(
  142. history_type_id: History.type_lookup('notification').id,
  143. history_object_id: History.object_lookup('Ticket').id,
  144. o_id: ticket.id
  145. ).where('created_at > ?', already_notified_cutoff).exists?(['value_to LIKE ?', "%#{SqlHelper.quote_like(identifier)}(#{SqlHelper.quote_like(@item[:type])}:%"])
  146. return if already_notified
  147. end
  148. # create online notification
  149. used_channels = []
  150. if channels['online']
  151. used_channels.push 'online'
  152. created_by_id = @item[:user_id] || 1
  153. # delete old notifications
  154. if @item[:type] == 'reminder_reached'
  155. seen = false
  156. created_by_id = 1
  157. OnlineNotification.remove_by_type('Ticket', ticket.id, @item[:type], user)
  158. elsif %w[escalation escalation_warning].include?(@item[:type])
  159. seen = false
  160. created_by_id = 1
  161. OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation', user)
  162. OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation_warning', user)
  163. # on updates without state changes create unseen messages
  164. elsif @item[:type] != 'create' && (@item[:changes].blank? || @item[:changes]['state_id'].blank?)
  165. seen = false
  166. else
  167. seen = OnlineNotification.seen_state?(ticket, user.id)
  168. end
  169. OnlineNotification.add(
  170. type: @item[:type],
  171. object: @item[:object],
  172. o_id: @item[:object].eql?('Ticket') ? ticket.id : article.id,
  173. seen: seen,
  174. created_by_id: created_by_id,
  175. user_id: user.id,
  176. )
  177. Rails.logger.debug { "sent ticket online notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email})" }
  178. end
  179. # ignore email channel notification and empty emails
  180. if !channels['email'] || user.email.blank?
  181. add_recipient_list_to_history(ticket, user, used_channels, @item[:type])
  182. return
  183. end
  184. used_channels.push 'email'
  185. add_recipient_list_to_history(ticket, user, used_channels, @item[:type])
  186. # get user based notification template
  187. # if create, send create message / block update messages
  188. template = case @item[:type]
  189. when 'create'
  190. 'ticket_create'
  191. when 'update'
  192. 'ticket_update'
  193. when 'reminder_reached'
  194. 'ticket_reminder_reached'
  195. when 'escalation'
  196. 'ticket_escalation'
  197. when 'escalation_warning'
  198. 'ticket_escalation_warning'
  199. when 'update.merged_into'
  200. 'ticket_update_merged_into'
  201. when 'update.received_merge'
  202. 'ticket_update_received_merge'
  203. when 'update.reaction'
  204. 'ticket_article_update_reaction'
  205. else
  206. raise "unknown type for notification #{@item[:type]}"
  207. end
  208. attachments = []
  209. if article
  210. attachments = article.attachments_inline
  211. end
  212. NotificationFactory::Mailer.notification(
  213. template: template,
  214. user: user,
  215. objects: {
  216. ticket: ticket,
  217. article: article,
  218. recipient: user,
  219. current_user: current_user,
  220. changes: changes,
  221. reason: recipients_reason[user.id],
  222. },
  223. message_id: "<notification.#{DateTime.current.to_fs(:number)}.#{ticket.id}.#{user.id}.#{SecureRandom.uuid}@#{Setting.get('fqdn')}>",
  224. references: ticket.get_references,
  225. main_object: ticket,
  226. attachments: attachments,
  227. )
  228. Rails.logger.debug { "sent ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email})" }
  229. rescue Channel::DeliveryError => e
  230. status_code = begin
  231. e.original_error.response.status.to_i
  232. rescue
  233. raise e
  234. end
  235. if SILENCABLE_SMTP_ERROR_CODES.any? { |elem| elem.include? status_code }
  236. Rails.logger.info do
  237. "could not send ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email}) #{e.original_error}"
  238. end
  239. return
  240. end
  241. raise e
  242. end
  243. def add_recipient_list_to_history(ticket, user, channels, type)
  244. return if channels.blank?
  245. identifier = user.email.presence || user.login
  246. recipient_list = "#{identifier}(#{type}:#{channels.join(',')})"
  247. History.add(
  248. o_id: ticket.id,
  249. history_type: 'notification',
  250. history_object: 'Ticket',
  251. value_to: recipient_list,
  252. created_by_id: @item[:user_id] || 1
  253. )
  254. end
  255. private
  256. def ooo_replacements(user:, replacements:, ticket:, reasons:)
  257. replacement = user.out_of_office_agent
  258. return if !replacement
  259. return if !TicketPolicy.new(replacement, ticket).agent_read_access?
  260. return if !replacements.add?(replacement)
  261. reasons[replacement.id] = __('You are receiving this because you are out-of-office replacement for a participant of this ticket.')
  262. end
  263. def possible_recipients_of_group(group_id)
  264. Rails.cache.fetch("User/notification/possible_recipients_of_group/#{group_id}/#{User.latest_change}", expires_in: 20.seconds) do
  265. User.group_access(group_id, 'full').sort_by(&:login)
  266. end
  267. end
  268. end