ticket.rb 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include CanCsvImport
  7. include ChecksHtmlSanitized
  8. include HasHistory
  9. include HasTags
  10. include HasSearchIndexBackend
  11. include HasOnlineNotifications
  12. include HasLinks
  13. include HasObjectManagerAttributes
  14. include HasTaskbars
  15. include Ticket::CallsStatsTicketReopenLog
  16. include Ticket::EnqueuesUserTicketCounterJob
  17. include Ticket::ResetsPendingTimeSeconds
  18. include Ticket::SetsCloseTime
  19. include Ticket::SetsOnlineNotificationSeen
  20. include Ticket::TouchesAssociations
  21. include Ticket::TriggersSubscriptions
  22. include Ticket::ChecksReopenAfterCertainTime
  23. include ::Ticket::Escalation
  24. include ::Ticket::Subject
  25. include ::Ticket::Assets
  26. include ::Ticket::SearchIndex
  27. include ::Ticket::Search
  28. include ::Ticket::MergeHistory
  29. store :preferences
  30. before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  31. before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
  32. # This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
  33. include Ticket::SetsLastOwnerUpdateTime
  34. include HasTransactionDispatcher
  35. # workflow checks should run after before_create and before_update callbacks
  36. include ChecksCoreWorkflow
  37. validates :group_id, presence: true
  38. activity_stream_permission 'ticket.agent'
  39. core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
  40. activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
  41. :create_article_type_id,
  42. :create_article_sender_id,
  43. :article_count,
  44. :first_response_at,
  45. :first_response_escalation_at,
  46. :first_response_in_min,
  47. :first_response_diff_in_min,
  48. :close_at,
  49. :close_escalation_at,
  50. :close_in_min,
  51. :close_diff_in_min,
  52. :update_escalation_at,
  53. :update_in_min,
  54. :update_diff_in_min,
  55. :last_close_at,
  56. :last_contact_at,
  57. :last_contact_agent_at,
  58. :last_contact_customer_at,
  59. :last_owner_update_at,
  60. :preferences
  61. search_index_attributes_relevant :organization_id,
  62. :group_id,
  63. :state_id,
  64. :priority_id
  65. history_attributes_ignored :create_article_type_id,
  66. :create_article_sender_id,
  67. :article_count,
  68. :preferences
  69. history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
  70. validates :note, length: { maximum: 250 }
  71. sanitized_html :note
  72. belongs_to :group, optional: true
  73. belongs_to :organization, optional: true
  74. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  75. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  76. has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
  77. has_many :mentions, as: :mentionable, dependent: :destroy
  78. has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  79. belongs_to :state, class_name: 'Ticket::State', optional: true
  80. belongs_to :priority, class_name: 'Ticket::Priority', optional: true
  81. belongs_to :owner, class_name: 'User', optional: true
  82. belongs_to :customer, class_name: 'User', optional: true
  83. belongs_to :created_by, class_name: 'User', optional: true
  84. belongs_to :updated_by, class_name: 'User', optional: true
  85. belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
  86. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
  87. association_attributes_ignored :flags, :mentions
  88. attr_accessor :callback_loop
  89. =begin
  90. processes tickets which have reached their pending time and sets next state_id
  91. processed_tickets = Ticket.process_pending
  92. returns
  93. processed_tickets = [<Ticket>, ...]
  94. =end
  95. def self.process_pending
  96. result = []
  97. # process pending action tickets
  98. pending_action = Ticket::StateType.find_by(name: 'pending action')
  99. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  100. .where.not(next_state_id: nil)
  101. if ticket_states_pending_action.present?
  102. next_state_map = {}
  103. ticket_states_pending_action.each do |state|
  104. next_state_map[state.id] = state.next_state_id
  105. end
  106. tickets = where(state_id: next_state_map.keys)
  107. .where('pending_time <= ?', Time.zone.now)
  108. tickets.find_each(batch_size: 500) do |ticket|
  109. Transaction.execute do
  110. ticket.state_id = next_state_map[ticket.state_id]
  111. ticket.updated_at = Time.zone.now
  112. ticket.updated_by_id = 1
  113. ticket.save!
  114. end
  115. result.push ticket
  116. end
  117. end
  118. # process pending reminder tickets
  119. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  120. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  121. if ticket_states_pending_reminder.present?
  122. reminder_state_map = {}
  123. ticket_states_pending_reminder.each do |state|
  124. reminder_state_map[state.id] = state.next_state_id
  125. end
  126. tickets = where(state_id: reminder_state_map.keys)
  127. .where('pending_time <= ?', Time.zone.now)
  128. tickets.find_each(batch_size: 500) do |ticket|
  129. article_id = nil
  130. article = Ticket::Article.last_customer_agent_article(ticket.id)
  131. if article
  132. article_id = article.id
  133. end
  134. # send notification
  135. TransactionJob.perform_now(
  136. object: 'Ticket',
  137. type: 'reminder_reached',
  138. object_id: ticket.id,
  139. article_id: article_id,
  140. user_id: 1,
  141. )
  142. result.push ticket
  143. end
  144. end
  145. result
  146. end
  147. def auto_assign(user)
  148. return if !persisted?
  149. return if Setting.get('ticket_auto_assignment').blank?
  150. return if owner_id != 1
  151. return if !TicketPolicy.new(user, self).full?
  152. user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
  153. return if user_ids_ignore.include?(user.id)
  154. ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
  155. return if ticket_auto_assignment_selector.blank?
  156. condition = ticket_auto_assignment_selector[:condition].merge(
  157. 'ticket.id' => {
  158. 'operator' => 'is',
  159. 'value' => id,
  160. }
  161. )
  162. ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
  163. return if ticket_count.to_i.zero?
  164. update!(owner: user)
  165. end
  166. =begin
  167. processes escalated tickets
  168. processed_tickets = Ticket.process_escalation
  169. returns
  170. processed_tickets = [<Ticket>, ...]
  171. =end
  172. def self.process_escalation
  173. result = []
  174. # fetch all escalated and soon to be escalating tickets
  175. where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
  176. article_id = nil
  177. article = Ticket::Article.last_customer_agent_article(ticket.id)
  178. if article
  179. article_id = article.id
  180. end
  181. # send escalation
  182. if ticket.escalation_at < Time.zone.now
  183. TransactionJob.perform_now(
  184. object: 'Ticket',
  185. type: 'escalation',
  186. object_id: ticket.id,
  187. article_id: article_id,
  188. user_id: 1,
  189. )
  190. result.push ticket
  191. next
  192. end
  193. # check if warning needs to be sent
  194. TransactionJob.perform_now(
  195. object: 'Ticket',
  196. type: 'escalation_warning',
  197. object_id: ticket.id,
  198. article_id: article_id,
  199. user_id: 1,
  200. )
  201. result.push ticket
  202. end
  203. result
  204. end
  205. =begin
  206. processes tickets which auto unassign time has reached
  207. processed_tickets = Ticket.process_auto_unassign
  208. returns
  209. processed_tickets = [<Ticket>, ...]
  210. =end
  211. def self.process_auto_unassign
  212. # process pending action tickets
  213. state_ids = Ticket::State.by_category(:work_on).pluck(:id)
  214. return [] if state_ids.blank?
  215. result = []
  216. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  217. return [] if groups.blank?
  218. groups.each do |group|
  219. next if group.assignment_timeout.blank?
  220. ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
  221. ticket_ids.each do |ticket_id|
  222. ticket = Ticket.find_by(id: ticket_id)
  223. next if !ticket
  224. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  225. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  226. Transaction.execute do
  227. ticket.owner_id = 1
  228. ticket.updated_at = Time.zone.now
  229. ticket.updated_by_id = 1
  230. ticket.save!
  231. end
  232. result.push ticket
  233. end
  234. end
  235. result
  236. end
  237. =begin
  238. merge tickets
  239. ticket = Ticket.find(123)
  240. result = ticket.merge_to(
  241. ticket_id: 123,
  242. user_id: 123,
  243. )
  244. returns
  245. result = true|false
  246. =end
  247. def merge_to(data)
  248. # prevent cross merging tickets
  249. target_ticket = Ticket.find_by(id: data[:ticket_id])
  250. raise 'no target ticket given' if !target_ticket
  251. raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
  252. # check different ticket ids
  253. raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id
  254. # update articles
  255. Transaction.execute context: 'merge' do
  256. Ticket::Article.where(ticket_id: id).each(&:touch)
  257. # quiet update of reassign of articles
  258. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
  259. # mark target ticket as updated
  260. # otherwise the "received_merge" history entry
  261. # will be the same as the last updated_at
  262. # which might be a long time ago
  263. target_ticket.updated_at = Time.zone.now
  264. # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
  265. target_ticket.history_log(
  266. 'received_merge',
  267. data[:user_id],
  268. id_to: target_ticket.id,
  269. id_from: id,
  270. )
  271. history_log(
  272. 'merged_into',
  273. data[:user_id],
  274. id_to: target_ticket.id,
  275. id_from: id,
  276. )
  277. # create new merge article
  278. Ticket::Article.create(
  279. ticket_id: id,
  280. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  281. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  282. body: 'merged',
  283. internal: false,
  284. created_by_id: data[:user_id],
  285. updated_by_id: data[:user_id],
  286. )
  287. # search for mention duplicates and destroy them before moving mentions
  288. Mention.duplicates(self, target_ticket).destroy_all
  289. Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
  290. # reassign links to the new ticket
  291. # rubocop:disable Rails/SkipsModelValidations
  292. ticket_source_id = Link::Object.find_by(name: 'Ticket').id
  293. # search for all duplicate source and target links and destroy them
  294. # before link merging
  295. Link.duplicates(
  296. object1_id: ticket_source_id,
  297. object1_value: id,
  298. object2_value: data[:ticket_id]
  299. ).destroy_all
  300. Link.where(
  301. link_object_source_id: ticket_source_id,
  302. link_object_source_value: id,
  303. ).update_all(link_object_source_value: data[:ticket_id])
  304. Link.where(
  305. link_object_target_id: ticket_source_id,
  306. link_object_target_value: id,
  307. ).update_all(link_object_target_value: data[:ticket_id])
  308. # rubocop:enable Rails/SkipsModelValidations
  309. # link tickets
  310. Link.add(
  311. link_type: 'parent',
  312. link_object_source: 'Ticket',
  313. link_object_source_value: data[:ticket_id],
  314. link_object_target: 'Ticket',
  315. link_object_target_value: id
  316. )
  317. # external sync references
  318. ExternalSync.migrate('Ticket', id, target_ticket.id)
  319. # set state to 'merged'
  320. self.state_id = Ticket::State.lookup(name: 'merged').id
  321. # rest owner
  322. self.owner_id = 1
  323. # save ticket
  324. save!
  325. # touch new ticket (to broadcast change)
  326. target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
  327. EventBuffer.add('transaction', {
  328. object: target_ticket.class.name,
  329. type: 'update.received_merge',
  330. data: target_ticket,
  331. changes: {},
  332. id: target_ticket.id,
  333. user_id: UserInfo.current_user_id,
  334. created_at: Time.zone.now,
  335. })
  336. EventBuffer.add('transaction', {
  337. object: self.class.name,
  338. type: 'update.merged_into',
  339. data: self,
  340. changes: {},
  341. id: id,
  342. user_id: UserInfo.current_user_id,
  343. created_at: Time.zone.now,
  344. })
  345. end
  346. true
  347. end
  348. =begin
  349. check if online notification should be shown in general as already seen with current state
  350. ticket = Ticket.find(1)
  351. seen = ticket.online_notification_seen_state(user_id_check)
  352. returns
  353. result = true # or false
  354. =end
  355. def online_notification_seen_state(user_id_check = nil)
  356. state = Ticket::State.lookup(id: state_id)
  357. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  358. # always to set unseen for ticket owner and users which did not the update
  359. return false if state_type.name != 'merged' && user_id_check && user_id_check == owner_id && user_id_check != updated_by_id
  360. # set all to seen if pending action state is a closed or merged state
  361. if state_type.name == 'pending action' && state.next_state_id
  362. state = Ticket::State.lookup(id: state.next_state_id)
  363. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  364. end
  365. # set all to seen if new state is pending reminder state
  366. if state_type.name == 'pending reminder'
  367. if user_id_check
  368. return false if owner_id == 1
  369. return false if updated_by_id != owner_id && user_id_check == owner_id
  370. return true
  371. end
  372. return true
  373. end
  374. # set all to seen if new state is a closed or merged state
  375. return true if state_type.name == 'closed'
  376. return true if state_type.name == 'merged'
  377. false
  378. end
  379. =begin
  380. get count of tickets and tickets which match on selector
  381. @param [Hash] selectors hash with conditions
  382. @oparam [Hash] options
  383. @option options [String] :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
  384. @option options [Integer] :limit of tickets to return
  385. @option options [User] :user is a current user
  386. @option options [Integer] :execution_time is a current user
  387. @return [Integer, [<Ticket>]]
  388. @example
  389. ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')
  390. ticket_count # count of found tickets
  391. tickets # tickets
  392. =end
  393. def self.selectors(selectors, options)
  394. limit = options[:limit] || 10
  395. current_user = options[:current_user]
  396. access = options[:access] || 'full'
  397. raise 'no selectors given' if !selectors
  398. query, bind_params, tables = selector2sql(selectors, options)
  399. return [] if !query
  400. ActiveRecord::Base.transaction(requires_new: true) do
  401. if !current_user || access == 'ignore'
  402. ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
  403. tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
  404. next [ticket_count, tickets]
  405. end
  406. tickets = "TicketPolicy::#{access.camelize}Scope".constantize
  407. .new(current_user).resolve
  408. .distinct
  409. .where(query, *bind_params)
  410. .joins(tables)
  411. next [tickets.count, tickets.limit(limit)]
  412. rescue ActiveRecord::StatementInvalid => e
  413. Rails.logger.error e
  414. raise ActiveRecord::Rollback
  415. end
  416. end
  417. =begin
  418. generate condition query to search for tickets based on condition
  419. query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)
  420. condition example
  421. {
  422. 'ticket.title' => {
  423. operator: 'contains', # contains not
  424. value: 'some value',
  425. },
  426. 'ticket.state_id' => {
  427. operator: 'is',
  428. value: [1,2,5]
  429. },
  430. 'ticket.created_at' => {
  431. operator: 'after (absolute)', # after,before
  432. value: '2015-10-17T06:00:00.000Z',
  433. },
  434. 'ticket.created_at' => {
  435. operator: 'within next (relative)', # within next, within last, after, before
  436. range: 'day', # minute|hour|day|month|year
  437. value: '25',
  438. },
  439. 'ticket.owner_id' => {
  440. operator: 'is', # is not
  441. pre_condition: 'current_user.id',
  442. },
  443. 'ticket.owner_id' => {
  444. operator: 'is', # is not
  445. pre_condition: 'specific',
  446. value: 4711,
  447. },
  448. 'ticket.escalation_at' => {
  449. operator: 'is not', # not
  450. value: nil,
  451. },
  452. 'ticket.tags' => {
  453. operator: 'contains all', # contains all|contains one|contains all not|contains one not
  454. value: 'tag1, tag2',
  455. },
  456. }
  457. =end
  458. def self.selector2sql(selectors, options = {})
  459. Ticket::Selector::Sql.new(selector: selectors, options: options).get
  460. end
  461. =begin
  462. perform changes on ticket
  463. ticket.perform_changes(trigger, 'trigger', item, current_user_id)
  464. # or
  465. ticket.perform_changes(job, 'job', item, current_user_id)
  466. =end
  467. def perform_changes(performable, perform_origin, item = nil, current_user_id = nil)
  468. perform = performable.perform
  469. logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }
  470. article = begin
  471. Ticket::Article.find_by(id: item.try(:dig, :article_id))
  472. rescue ArgumentError
  473. nil
  474. end
  475. # if the configuration contains the deletion of the ticket then
  476. # we skip all other ticket changes because they does not matter
  477. if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
  478. perform.each_key do |key|
  479. (object_name, attribute) = key.split('.', 2)
  480. next if object_name != 'ticket'
  481. next if attribute == 'action'
  482. perform.delete(key)
  483. end
  484. end
  485. objects = build_notification_template_objects(article)
  486. perform_notification = {}
  487. perform_article = {}
  488. changed = false
  489. perform.each do |key, value|
  490. (object_name, attribute) = key.split('.', 2)
  491. raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification'
  492. # send notification/create article (after changes are done)
  493. if object_name == 'article'
  494. perform_article[key] = value
  495. next
  496. end
  497. if object_name == 'notification'
  498. perform_notification[key] = value
  499. next
  500. end
  501. # Apply pending_time changes
  502. if key == 'ticket.pending_time'
  503. new_value = case value['operator']
  504. when 'static'
  505. value['value']
  506. when 'relative'
  507. TimeRangeHelper.relative(range: value['range'], value: value['value'])
  508. end
  509. if new_value
  510. self[attribute] = new_value
  511. changed = true
  512. next
  513. end
  514. end
  515. # update tags
  516. if key == 'ticket.tags'
  517. next if value['value'].blank?
  518. tags = value['value'].split(',')
  519. case value['operator']
  520. when 'add'
  521. tags.each do |tag|
  522. tag_add(tag, current_user_id || 1)
  523. end
  524. when 'remove'
  525. tags.each do |tag|
  526. tag_remove(tag, current_user_id || 1)
  527. end
  528. else
  529. logger.error "Unknown #{attribute} operator #{value['operator']}"
  530. end
  531. next
  532. end
  533. # delete ticket
  534. if key == 'ticket.action'
  535. next if value['value'].blank?
  536. next if value['value'] != 'delete'
  537. logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
  538. destroy!
  539. next
  540. end
  541. # lookup pre_condition
  542. if value['pre_condition']
  543. if value['pre_condition'].start_with?('not_set')
  544. value['value'] = 1
  545. elsif value['pre_condition'].start_with?('current_user.')
  546. raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
  547. value['value'] = current_user_id
  548. end
  549. end
  550. # update ticket
  551. next if self[attribute].to_s == value['value'].to_s
  552. changed = true
  553. if value['value'].is_a?(String)
  554. value['value'] = NotificationFactory::Mailer.template(
  555. templateInline: value['value'],
  556. objects: objects,
  557. quote: true,
  558. )
  559. end
  560. self[attribute] = value['value']
  561. logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
  562. end
  563. if changed
  564. save!
  565. end
  566. perform_article.each do |key, value|
  567. raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
  568. add_trigger_note(id, value, objects, perform_origin)
  569. end
  570. perform_notification.each do |key, value|
  571. # send notification
  572. case key
  573. when 'notification.sms'
  574. send_sms_notification(value, article, perform_origin)
  575. next
  576. when 'notification.email'
  577. send_email_notification(value, article, perform_origin)
  578. when 'notification.webhook'
  579. TriggerWebhookJob.perform_later(performable, self, article)
  580. end
  581. end
  582. true
  583. end
  584. =begin
  585. perform changes on ticket
  586. ticket.add_trigger_note(ticket_id, note, objects, perform_origin)
  587. =end
  588. def add_trigger_note(ticket_id, note, objects, perform_origin)
  589. rendered_subject = NotificationFactory::Mailer.template(
  590. templateInline: note[:subject],
  591. objects: objects,
  592. quote: true,
  593. )
  594. rendered_body = NotificationFactory::Mailer.template(
  595. templateInline: note[:body],
  596. objects: objects,
  597. quote: true,
  598. )
  599. Ticket::Article.create!(
  600. ticket_id: ticket_id,
  601. subject: rendered_subject,
  602. content_type: 'text/html',
  603. body: rendered_body,
  604. internal: note[:internal],
  605. sender: Ticket::Article::Sender.find_by(name: 'System'),
  606. type: Ticket::Article::Type.find_by(name: 'note'),
  607. preferences: {
  608. perform_origin: perform_origin,
  609. notification: true,
  610. },
  611. updated_by_id: 1,
  612. created_by_id: 1,
  613. )
  614. end
  615. =begin
  616. perform active triggers on ticket
  617. Ticket.perform_triggers(ticket, article, item, options)
  618. =end
  619. def self.perform_triggers(ticket, article, item, options = {})
  620. recursive = Setting.get('ticket_trigger_recursive')
  621. type = options[:type] || item[:type]
  622. local_options = options.clone
  623. local_options[:type] = type
  624. local_options[:reset_user_id] = true
  625. local_options[:disable] = ['Transaction::Notification']
  626. local_options[:trigger_ids] ||= {}
  627. local_options[:trigger_ids][ticket.id.to_s] ||= []
  628. local_options[:loop_count] ||= 0
  629. local_options[:loop_count] += 1
  630. ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
  631. if local_options[:loop_count] > ticket_trigger_recursive_max_loop
  632. message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
  633. logger.info { message }
  634. return [false, message]
  635. end
  636. triggers = if Rails.configuration.db_case_sensitive
  637. ::Trigger.where(active: true).order(Arel.sql('LOWER(name)'))
  638. else
  639. ::Trigger.where(active: true).order(:name)
  640. end
  641. return [true, __('No triggers active')] if triggers.blank?
  642. # check if notification should be send because of customer emails
  643. send_notification = true
  644. if local_options[:send_notification] == false
  645. send_notification = false
  646. elsif item[:article_id]
  647. article = Ticket::Article.lookup(id: item[:article_id])
  648. if article&.preferences && article.preferences['send-auto-response'] == false
  649. send_notification = false
  650. end
  651. end
  652. Transaction.execute(local_options) do
  653. triggers.each do |trigger|
  654. logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  655. user_id = ticket.updated_by_id
  656. if article
  657. user_id = article.updated_by_id
  658. end
  659. user = User.lookup(id: user_id)
  660. # verify is condition is matching
  661. ticket_count, tickets = Ticket.selectors(trigger.condition, limit: 1, execution_time: true, current_user: user, access: 'ignore', ticket_action: type, ticket_id: ticket.id, article_id: article&.id, changes: item[:changes], changes_required: true)
  662. next if ticket_count.blank?
  663. next if ticket_count.zero?
  664. next if tickets.first.id != ticket.id
  665. if recursive == false && local_options[:loop_count] > 1
  666. message = "Do not execute recursive triggers per default until Zammad 3.0. With Zammad 3.0 and higher the following trigger is executed '#{trigger.name}' on Ticket:#{ticket.id}. Please review your current triggers and change them if needed."
  667. logger.info { message }
  668. return [true, message]
  669. end
  670. if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
  671. recipient = trigger.perform['notification.email']['recipient']
  672. local_options[:send_notification] = false
  673. if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
  674. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
  675. next
  676. end
  677. end
  678. if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
  679. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  680. next
  681. end
  682. local_options[:trigger_ids][ticket.id.to_s].push trigger.id
  683. logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  684. ticket.perform_changes(trigger, 'trigger', item, user_id)
  685. if recursive == true
  686. TransactionDispatcher.commit(local_options)
  687. end
  688. end
  689. end
  690. [true, ticket, local_options]
  691. end
  692. =begin
  693. get all email references headers of a ticket, to exclude some, parse it as array into method
  694. references = ticket.get_references
  695. result
  696. ['message-id-1234', 'message-id-5678']
  697. ignore references header(s)
  698. references = ticket.get_references(['message-id-5678'])
  699. result
  700. ['message-id-1234']
  701. =end
  702. def get_references(ignore = [])
  703. references = []
  704. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
  705. if article.in_reply_to.present?
  706. references.push article.in_reply_to
  707. end
  708. next if article.message_id.blank?
  709. references.push article.message_id
  710. end
  711. ignore.each do |item|
  712. references.delete(item)
  713. end
  714. references
  715. end
  716. =begin
  717. get all articles of a ticket in correct order (overwrite active record default method)
  718. articles = ticket.articles
  719. result
  720. [article1, article2]
  721. =end
  722. def articles
  723. Ticket::Article.where(ticket_id: id).order(:created_at, :id)
  724. end
  725. # Get whichever #last_contact_* was later
  726. # This is not identical to #last_contact_at
  727. # It returns time to last original (versus follow up) contact
  728. # @return [Time, nil]
  729. def last_original_update_at
  730. [last_contact_agent_at, last_contact_customer_at].compact.max
  731. end
  732. # true if conversation did happen and agent responded
  733. # false if customer is waiting for response or agent reached out and customer did not respond yet
  734. # @return [Bool]
  735. def agent_responded?
  736. return false if last_contact_customer_at.blank?
  737. return false if last_contact_agent_at.blank?
  738. last_contact_customer_at < last_contact_agent_at
  739. end
  740. private
  741. def check_generate
  742. return true if number
  743. self.number = Ticket::Number.generate
  744. true
  745. end
  746. def check_title
  747. return true if !title
  748. title.gsub!(%r{\s|\t|\r}, ' ')
  749. true
  750. end
  751. def check_defaults
  752. if !owner_id
  753. self.owner_id = 1
  754. end
  755. return true if !customer_id
  756. customer = User.find_by(id: customer_id)
  757. return true if !customer
  758. return true if organization_id.present? && customer.organization_id?(organization_id)
  759. self.organization_id = customer.organization_id
  760. true
  761. end
  762. def reset_pending_time
  763. # ignore if no state has changed
  764. return true if !changes_to_save['state_id']
  765. # ignore if new state is blank and
  766. # let handle ActiveRecord the error
  767. return if state_id.blank?
  768. # check if new state isn't pending*
  769. current_state = Ticket::State.lookup(id: state_id)
  770. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  771. # in case, set pending_time to nil
  772. return true if current_state_type.name.match?(%r{^pending}i)
  773. self.pending_time = nil
  774. true
  775. end
  776. def set_default_state
  777. return true if state_id
  778. default_ticket_state = Ticket::State.find_by(default_create: true)
  779. return true if !default_ticket_state
  780. self.state_id = default_ticket_state.id
  781. true
  782. end
  783. def set_default_priority
  784. return true if priority_id
  785. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  786. return true if !default_ticket_priority
  787. self.priority_id = default_ticket_priority.id
  788. true
  789. end
  790. def check_owner_active
  791. return true if Setting.get('import_mode')
  792. # only change the owner for non closed Tickets for historical/reporting reasons
  793. return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
  794. # return when ticket is unassigned
  795. return true if owner_id.blank?
  796. return true if owner_id == 1
  797. # return if owner is active, is agent and has access to group of ticket
  798. return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
  799. # else set the owner of the ticket to the default user as unassigned
  800. self.owner_id = 1
  801. true
  802. end
  803. # articles.last breaks (returns the wrong article)
  804. # if another email notification trigger preceded this one
  805. # (see https://github.com/zammad/zammad/issues/1543)
  806. def build_notification_template_objects(article)
  807. last_article = nil
  808. last_internal_article = nil
  809. last_external_article = nil
  810. all_articles = articles
  811. if article.nil?
  812. last_article = all_articles.last
  813. last_internal_article = all_articles.reverse.find(&:internal?)
  814. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  815. else
  816. last_article = article
  817. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  818. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  819. end
  820. {
  821. ticket: self,
  822. article: last_article,
  823. last_article: last_article,
  824. last_internal_article: last_internal_article,
  825. last_external_article: last_external_article,
  826. created_article: article,
  827. created_internal_article: article&.internal? ? article : nil,
  828. created_external_article: article&.internal? ? nil : article,
  829. }
  830. end
  831. def send_email_notification(value, article, perform_origin)
  832. # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
  833. value_recipient = Array(value['recipient'])
  834. recipients_raw = []
  835. value_recipient.each do |recipient|
  836. case recipient
  837. when 'article_last_sender'
  838. if article.present?
  839. if article.reply_to.present?
  840. recipients_raw.push(article.reply_to)
  841. elsif article.from.present?
  842. recipients_raw.push(article.from)
  843. elsif article.origin_by_id
  844. email = User.find_by(id: article.origin_by_id).email
  845. recipients_raw.push(email)
  846. elsif article.created_by_id
  847. email = User.find_by(id: article.created_by_id).email
  848. recipients_raw.push(email)
  849. end
  850. end
  851. when 'ticket_customer'
  852. email = User.find_by(id: customer_id).email
  853. recipients_raw.push(email)
  854. when 'ticket_owner'
  855. email = User.find_by(id: owner_id).email
  856. recipients_raw.push(email)
  857. when 'ticket_agents'
  858. User.group_access(group_id, 'full').sort_by(&:login).each do |user|
  859. recipients_raw.push(user.email)
  860. end
  861. when %r{\Auserid_(\d+)\z}
  862. user = User.lookup(id: $1)
  863. if !user
  864. logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
  865. next
  866. end
  867. recipients_raw.push(user.email)
  868. else
  869. logger.error "Unknown email notification recipient '#{recipient}'"
  870. next
  871. end
  872. end
  873. recipients_checked = []
  874. recipients_raw.each do |recipient_email|
  875. users = User.where(email: recipient_email)
  876. next if users.any? { |user| !trigger_based_notification?(user) }
  877. # send notifications only to email addresses
  878. next if recipient_email.blank?
  879. # check if address is valid
  880. begin
  881. Mail::AddressList.new(recipient_email).addresses.each do |address|
  882. recipient_email = address.address
  883. email_address_validation = EmailAddressValidation.new(recipient_email)
  884. break if recipient_email.present? && email_address_validation.valid?
  885. end
  886. rescue
  887. if recipient_email.present?
  888. if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
  889. next # no usable format found
  890. end
  891. recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
  892. end
  893. end
  894. email_address_validation = EmailAddressValidation.new(recipient_email)
  895. next if !email_address_validation.valid?
  896. # do not send notification if system address
  897. next if EmailAddress.exists?(email: recipient_email.downcase)
  898. # do not sent notifications to this recipients
  899. send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
  900. begin
  901. next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
  902. rescue => e
  903. logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
  904. logger.error e
  905. next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
  906. end
  907. # check if notification should be send because of customer emails
  908. if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
  909. logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
  910. next
  911. end
  912. # loop protection / check if maximal count of trigger mail has reached
  913. map = {
  914. 10 => 10,
  915. 30 => 15,
  916. 60 => 25,
  917. 180 => 50,
  918. 600 => 100,
  919. }
  920. skip = false
  921. map.each do |minutes, count|
  922. already_sent = Ticket::Article.where(
  923. ticket_id: id,
  924. sender: Ticket::Article::Sender.find_by(name: 'System'),
  925. type: Ticket::Article::Type.find_by(name: 'email'),
  926. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  927. next if already_sent < count
  928. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
  929. skip = true
  930. break
  931. end
  932. next if skip
  933. map = {
  934. 10 => 30,
  935. 30 => 60,
  936. 60 => 120,
  937. 180 => 240,
  938. 600 => 360,
  939. }
  940. skip = false
  941. map.each do |minutes, count|
  942. already_sent = Ticket::Article.where(
  943. sender: Ticket::Article::Sender.find_by(name: 'System'),
  944. type: Ticket::Article::Type.find_by(name: 'email'),
  945. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  946. next if already_sent < count
  947. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
  948. skip = true
  949. break
  950. end
  951. next if skip
  952. email = recipient_email.downcase.strip
  953. next if recipients_checked.include?(email)
  954. recipients_checked.push(email)
  955. end
  956. return if recipients_checked.blank?
  957. recipient_string = recipients_checked.join(', ')
  958. group_id = self.group_id
  959. return if !group_id
  960. email_address = Group.find(group_id).email_address
  961. if !email_address
  962. logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
  963. return
  964. end
  965. if !email_address.channel_id
  966. logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
  967. return
  968. end
  969. security = nil
  970. if Setting.get('smime_integration')
  971. sign = value['sign'].present? && value['sign'] != 'no'
  972. encryption = value['encryption'].present? && value['encryption'] != 'no'
  973. security = {
  974. type: 'S/MIME',
  975. sign: {
  976. success: false,
  977. },
  978. encryption: {
  979. success: false,
  980. },
  981. }
  982. if sign
  983. sign_found = false
  984. begin
  985. list = Mail::AddressList.new(email_address.email)
  986. from = list.addresses.first.to_s
  987. cert = SMIMECertificate.for_sender_email_address(from)
  988. if cert && !cert.expired?
  989. sign_found = true
  990. security[:sign][:success] = true
  991. security[:sign][:comment] = "certificate for #{email_address.email} found"
  992. end
  993. rescue # rubocop:disable Lint/SuppressedException
  994. end
  995. if value['sign'] == 'discard' && !sign_found
  996. logger.info "Unable to send trigger based notification to #{recipient_string} because of missing group #{group.name} email #{email_address.email} certificate for signing (discarding notification)."
  997. return
  998. end
  999. end
  1000. if encryption
  1001. certs_found = false
  1002. begin
  1003. SMIMECertificate.for_recipipent_email_addresses!(recipients_checked)
  1004. certs_found = true
  1005. security[:encryption][:success] = true
  1006. security[:encryption][:comment] = "certificates found for #{recipient_string}"
  1007. rescue # rubocop:disable Lint/SuppressedException
  1008. end
  1009. if value['encryption'] == 'discard' && !certs_found
  1010. logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
  1011. return
  1012. end
  1013. end
  1014. end
  1015. objects = build_notification_template_objects(article)
  1016. # get subject
  1017. subject = NotificationFactory::Mailer.template(
  1018. templateInline: value['subject'],
  1019. objects: objects,
  1020. quote: false,
  1021. )
  1022. subject = subject_build(subject)
  1023. body = NotificationFactory::Mailer.template(
  1024. templateInline: value['body'],
  1025. objects: objects,
  1026. quote: true,
  1027. )
  1028. (body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
  1029. preferences = {}
  1030. preferences[:perform_origin] = perform_origin
  1031. if security.present?
  1032. preferences[:security] = security
  1033. end
  1034. message = Ticket::Article.create(
  1035. ticket_id: id,
  1036. to: recipient_string,
  1037. subject: subject,
  1038. content_type: 'text/html',
  1039. body: body,
  1040. internal: value['internal'] || false, # default to public if value was not set
  1041. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1042. type: Ticket::Article::Type.find_by(name: 'email'),
  1043. preferences: preferences,
  1044. updated_by_id: 1,
  1045. created_by_id: 1,
  1046. )
  1047. attachments_inline.each do |attachment|
  1048. Store.create!(
  1049. object: 'Ticket::Article',
  1050. o_id: message.id,
  1051. data: attachment[:data],
  1052. filename: attachment[:filename],
  1053. preferences: attachment[:preferences],
  1054. )
  1055. end
  1056. original_article = objects[:article]
  1057. if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
  1058. original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
  1059. end
  1060. if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
  1061. original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
  1062. original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
  1063. end
  1064. end
  1065. def sms_recipients_by_type(recipient_type, article)
  1066. case recipient_type
  1067. when 'article_last_sender'
  1068. return nil if article.blank?
  1069. if article.origin_by_id
  1070. article.origin_by_id
  1071. elsif article.created_by_id
  1072. article.created_by_id
  1073. end
  1074. when 'ticket_customer'
  1075. customer_id
  1076. when 'ticket_owner'
  1077. owner_id
  1078. when 'ticket_agents'
  1079. User.group_access(group_id, 'full').sort_by(&:login)
  1080. when %r{\Auserid_(\d+)\z}
  1081. return $1 if User.exists?($1)
  1082. logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
  1083. nil
  1084. else
  1085. logger.error "Unknown sms notification recipient '#{recipient}'"
  1086. nil
  1087. end
  1088. end
  1089. def build_sms_recipients_list(value, article)
  1090. Array(value['recipient'])
  1091. .each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
  1092. .map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
  1093. .uniq(&:id)
  1094. .select { |user| user.mobile.present? }
  1095. end
  1096. def send_sms_notification(value, article, perform_origin)
  1097. sms_recipients = build_sms_recipients_list(value, article)
  1098. if sms_recipients.blank?
  1099. logger.debug "No SMS recipients found for Ticket# #{number}"
  1100. return
  1101. end
  1102. sms_recipients_to = sms_recipients
  1103. .map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
  1104. .join(', ')
  1105. channel = Channel.find_by(area: 'Sms::Notification')
  1106. if !channel.active?
  1107. # write info message since we have an active trigger
  1108. logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
  1109. return
  1110. end
  1111. objects = build_notification_template_objects(article)
  1112. body = NotificationFactory::Renderer.new(
  1113. objects: objects,
  1114. template: value['body'],
  1115. escape: false
  1116. ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space
  1117. # attributes content_type is not needed for SMS
  1118. Ticket::Article.create(
  1119. ticket_id: id,
  1120. subject: 'SMS notification',
  1121. to: sms_recipients_to,
  1122. body: body,
  1123. internal: value['internal'] || false, # default to public if value was not set
  1124. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1125. type: Ticket::Article::Type.find_by(name: 'sms'),
  1126. preferences: {
  1127. perform_origin: perform_origin,
  1128. sms_recipients: sms_recipients.map(&:mobile),
  1129. channel_id: channel.id,
  1130. },
  1131. updated_by_id: 1,
  1132. created_by_id: 1,
  1133. )
  1134. end
  1135. def trigger_based_notification?(user)
  1136. blocked_in_days = trigger_based_notification_blocked_in_days(user)
  1137. return true if blocked_in_days.zero?
  1138. logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
  1139. false
  1140. end
  1141. def trigger_based_notification_blocked_in_days(user)
  1142. return 0 if !user.preferences[:mail_delivery_failed]
  1143. return 0 if user.preferences[:mail_delivery_failed_data].blank?
  1144. # blocked for 60 full days; see #4459
  1145. remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  1146. return remaining_days if remaining_days.positive?
  1147. # cleanup user preferences
  1148. user.preferences[:mail_delivery_failed] = false
  1149. user.preferences[:mail_delivery_failed_data] = nil
  1150. user.save!
  1151. 0
  1152. end
  1153. end