ticket.rb 48 KB

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