ticket.rb 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820
  1. # Copyright (C) 2012-2022 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 HasKarmaActivityLog
  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::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. activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
  40. :create_article_type_id,
  41. :create_article_sender_id,
  42. :article_count,
  43. :first_response_at,
  44. :first_response_escalation_at,
  45. :first_response_in_min,
  46. :first_response_diff_in_min,
  47. :close_at,
  48. :close_escalation_at,
  49. :close_in_min,
  50. :close_diff_in_min,
  51. :update_escalation_at,
  52. :update_in_min,
  53. :update_diff_in_min,
  54. :last_contact_at,
  55. :last_contact_agent_at,
  56. :last_contact_customer_at,
  57. :last_owner_update_at,
  58. :preferences
  59. search_index_attributes_relevant :organization_id,
  60. :group_id,
  61. :state_id,
  62. :priority_id
  63. history_attributes_ignored :create_article_type_id,
  64. :create_article_sender_id,
  65. :article_count,
  66. :preferences
  67. history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
  68. validates :note, length: { maximum: 250 }
  69. sanitized_html :note
  70. belongs_to :group, optional: true
  71. belongs_to :organization, optional: true
  72. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  73. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  74. has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
  75. has_many :mentions, as: :mentionable, dependent: :destroy
  76. has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  77. belongs_to :state, class_name: 'Ticket::State', optional: true
  78. belongs_to :priority, class_name: 'Ticket::Priority', optional: true
  79. belongs_to :owner, class_name: 'User', optional: true
  80. belongs_to :customer, class_name: 'User', optional: true
  81. belongs_to :created_by, class_name: 'User', optional: true
  82. belongs_to :updated_by, class_name: 'User', optional: true
  83. belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
  84. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
  85. association_attributes_ignored :flags, :mentions
  86. attr_accessor :callback_loop
  87. =begin
  88. processes tickets which have reached their pending time and sets next state_id
  89. processed_tickets = Ticket.process_pending
  90. returns
  91. processed_tickets = [<Ticket>, ...]
  92. =end
  93. def self.process_pending
  94. result = []
  95. # process pending action tickets
  96. pending_action = Ticket::StateType.find_by(name: 'pending action')
  97. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  98. .where.not(next_state_id: nil)
  99. if ticket_states_pending_action.present?
  100. next_state_map = {}
  101. ticket_states_pending_action.each do |state|
  102. next_state_map[state.id] = state.next_state_id
  103. end
  104. tickets = where(state_id: next_state_map.keys)
  105. .where('pending_time <= ?', Time.zone.now)
  106. tickets.find_each(batch_size: 500) do |ticket|
  107. Transaction.execute do
  108. ticket.state_id = next_state_map[ticket.state_id]
  109. ticket.updated_at = Time.zone.now
  110. ticket.updated_by_id = 1
  111. ticket.save!
  112. end
  113. result.push ticket
  114. end
  115. end
  116. # process pending reminder tickets
  117. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  118. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  119. if ticket_states_pending_reminder.present?
  120. reminder_state_map = {}
  121. ticket_states_pending_reminder.each do |state|
  122. reminder_state_map[state.id] = state.next_state_id
  123. end
  124. tickets = where(state_id: reminder_state_map.keys)
  125. .where('pending_time <= ?', Time.zone.now)
  126. tickets.find_each(batch_size: 500) do |ticket|
  127. article_id = nil
  128. article = Ticket::Article.last_customer_agent_article(ticket.id)
  129. if article
  130. article_id = article.id
  131. end
  132. # send notification
  133. TransactionJob.perform_now(
  134. object: 'Ticket',
  135. type: 'reminder_reached',
  136. object_id: ticket.id,
  137. article_id: article_id,
  138. user_id: 1,
  139. )
  140. result.push ticket
  141. end
  142. end
  143. result
  144. end
  145. =begin
  146. processes escalated tickets
  147. processed_tickets = Ticket.process_escalation
  148. returns
  149. processed_tickets = [<Ticket>, ...]
  150. =end
  151. def self.process_escalation
  152. result = []
  153. # fetch all escalated and soon to be escalating tickets
  154. where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
  155. article_id = nil
  156. article = Ticket::Article.last_customer_agent_article(ticket.id)
  157. if article
  158. article_id = article.id
  159. end
  160. # send escalation
  161. if ticket.escalation_at < Time.zone.now
  162. TransactionJob.perform_now(
  163. object: 'Ticket',
  164. type: 'escalation',
  165. object_id: ticket.id,
  166. article_id: article_id,
  167. user_id: 1,
  168. )
  169. result.push ticket
  170. next
  171. end
  172. # check if warning needs to be sent
  173. TransactionJob.perform_now(
  174. object: 'Ticket',
  175. type: 'escalation_warning',
  176. object_id: ticket.id,
  177. article_id: article_id,
  178. user_id: 1,
  179. )
  180. result.push ticket
  181. end
  182. result
  183. end
  184. =begin
  185. processes tickets which auto unassign time has reached
  186. processed_tickets = Ticket.process_auto_unassign
  187. returns
  188. processed_tickets = [<Ticket>, ...]
  189. =end
  190. def self.process_auto_unassign
  191. # process pending action tickets
  192. state_ids = Ticket::State.by_category(:work_on).pluck(:id)
  193. return [] if state_ids.blank?
  194. result = []
  195. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  196. return [] if groups.blank?
  197. groups.each do |group|
  198. next if group.assignment_timeout.blank?
  199. 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)
  200. ticket_ids.each do |ticket_id|
  201. ticket = Ticket.find_by(id: ticket_id)
  202. next if !ticket
  203. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  204. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  205. Transaction.execute do
  206. ticket.owner_id = 1
  207. ticket.updated_at = Time.zone.now
  208. ticket.updated_by_id = 1
  209. ticket.save!
  210. end
  211. result.push ticket
  212. end
  213. end
  214. result
  215. end
  216. =begin
  217. merge tickets
  218. ticket = Ticket.find(123)
  219. result = ticket.merge_to(
  220. ticket_id: 123,
  221. user_id: 123,
  222. )
  223. returns
  224. result = true|false
  225. =end
  226. def merge_to(data)
  227. # prevent cross merging tickets
  228. target_ticket = Ticket.find_by(id: data[:ticket_id])
  229. raise 'no target ticket given' if !target_ticket
  230. raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
  231. # check different ticket ids
  232. raise Exceptions::UnprocessableEntity, __('Can\'t merge ticket with itself!') if id == target_ticket.id
  233. # update articles
  234. Transaction.execute context: 'merge' do
  235. Ticket::Article.where(ticket_id: id).each(&:touch)
  236. # quiet update of reassign of articles
  237. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
  238. # mark target ticket as updated
  239. # otherwise the "received_merge" history entry
  240. # will be the same as the last updated_at
  241. # which might be a long time ago
  242. target_ticket.updated_at = Time.zone.now
  243. # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
  244. target_ticket.history_log(
  245. 'received_merge',
  246. data[:user_id],
  247. id_to: target_ticket.id,
  248. id_from: id,
  249. )
  250. history_log(
  251. 'merged_into',
  252. data[:user_id],
  253. id_to: target_ticket.id,
  254. id_from: id,
  255. )
  256. # create new merge article
  257. Ticket::Article.create(
  258. ticket_id: id,
  259. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  260. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  261. body: 'merged',
  262. internal: false,
  263. created_by_id: data[:user_id],
  264. updated_by_id: data[:user_id],
  265. )
  266. # search for mention duplicates and destroy them before moving mentions
  267. Mention.duplicates(self, target_ticket).destroy_all
  268. Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
  269. # reassign links to the new ticket
  270. # rubocop:disable Rails/SkipsModelValidations
  271. ticket_source_id = Link::Object.find_by(name: 'Ticket').id
  272. # search for all duplicate source and target links and destroy them
  273. # before link merging
  274. Link.duplicates(
  275. object1_id: ticket_source_id,
  276. object1_value: id,
  277. object2_value: data[:ticket_id]
  278. ).destroy_all
  279. Link.where(
  280. link_object_source_id: ticket_source_id,
  281. link_object_source_value: id,
  282. ).update_all(link_object_source_value: data[:ticket_id])
  283. Link.where(
  284. link_object_target_id: ticket_source_id,
  285. link_object_target_value: id,
  286. ).update_all(link_object_target_value: data[:ticket_id])
  287. # rubocop:enable Rails/SkipsModelValidations
  288. # link tickets
  289. Link.add(
  290. link_type: 'parent',
  291. link_object_source: 'Ticket',
  292. link_object_source_value: data[:ticket_id],
  293. link_object_target: 'Ticket',
  294. link_object_target_value: id
  295. )
  296. # external sync references
  297. ExternalSync.migrate('Ticket', id, target_ticket.id)
  298. # set state to 'merged'
  299. self.state_id = Ticket::State.lookup(name: 'merged').id
  300. # rest owner
  301. self.owner_id = 1
  302. # save ticket
  303. save!
  304. # touch new ticket (to broadcast change)
  305. target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
  306. EventBuffer.add('transaction', {
  307. object: target_ticket.class.name,
  308. type: 'update.received_merge',
  309. data: target_ticket,
  310. changes: {},
  311. id: target_ticket.id,
  312. user_id: UserInfo.current_user_id,
  313. created_at: Time.zone.now,
  314. })
  315. EventBuffer.add('transaction', {
  316. object: self.class.name,
  317. type: 'update.merged_into',
  318. data: self,
  319. changes: {},
  320. id: id,
  321. user_id: UserInfo.current_user_id,
  322. created_at: Time.zone.now,
  323. })
  324. end
  325. true
  326. end
  327. =begin
  328. check if online notification should be shown in general as already seen with current state
  329. ticket = Ticket.find(1)
  330. seen = ticket.online_notification_seen_state(user_id_check)
  331. returns
  332. result = true # or false
  333. =end
  334. def online_notification_seen_state(user_id_check = nil)
  335. state = Ticket::State.lookup(id: state_id)
  336. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  337. # always to set unseen for ticket owner and users which did not the update
  338. return false if state_type.name != 'merged' && user_id_check && user_id_check == owner_id && user_id_check != updated_by_id
  339. # set all to seen if pending action state is a closed or merged state
  340. if state_type.name == 'pending action' && state.next_state_id
  341. state = Ticket::State.lookup(id: state.next_state_id)
  342. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  343. end
  344. # set all to seen if new state is pending reminder state
  345. if state_type.name == 'pending reminder'
  346. if user_id_check
  347. return false if owner_id == 1
  348. return false if updated_by_id != owner_id && user_id_check == owner_id
  349. return true
  350. end
  351. return true
  352. end
  353. # set all to seen if new state is a closed or merged state
  354. return true if state_type.name == 'closed'
  355. return true if state_type.name == 'merged'
  356. false
  357. end
  358. =begin
  359. get count of tickets and tickets which match on selector
  360. @param [Hash] selectors hash with conditions
  361. @oparam [Hash] options
  362. @option options [String] :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
  363. @option options [Integer] :limit of tickets to return
  364. @option options [User] :user is a current user
  365. @option options [Integer] :execution_time is a current user
  366. @return [Integer, [<Ticket>]]
  367. @example
  368. ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')
  369. ticket_count # count of found tickets
  370. tickets # tickets
  371. =end
  372. def self.selectors(selectors, options)
  373. limit = options[:limit] || 10
  374. current_user = options[:current_user]
  375. access = options[:access] || 'full'
  376. raise 'no selectors given' if !selectors
  377. query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
  378. return [] if !query
  379. ActiveRecord::Base.transaction(requires_new: true) do
  380. if !current_user || access == 'ignore'
  381. ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
  382. tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
  383. next [ticket_count, tickets]
  384. end
  385. tickets = "TicketPolicy::#{access.camelize}Scope".constantize
  386. .new(current_user).resolve
  387. .distinct
  388. .where(query, *bind_params)
  389. .joins(tables)
  390. next [tickets.count, tickets.limit(limit)]
  391. rescue ActiveRecord::StatementInvalid => e
  392. Rails.logger.error e
  393. raise ActiveRecord::Rollback
  394. end
  395. end
  396. =begin
  397. generate condition query to search for tickets based on condition
  398. query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)
  399. condition example
  400. {
  401. 'ticket.title' => {
  402. operator: 'contains', # contains not
  403. value: 'some value',
  404. },
  405. 'ticket.state_id' => {
  406. operator: 'is',
  407. value: [1,2,5]
  408. },
  409. 'ticket.created_at' => {
  410. operator: 'after (absolute)', # after,before
  411. value: '2015-10-17T06:00:00.000Z',
  412. },
  413. 'ticket.created_at' => {
  414. operator: 'within next (relative)', # within next, within last, after, before
  415. range: 'day', # minute|hour|day|month|year
  416. value: '25',
  417. },
  418. 'ticket.owner_id' => {
  419. operator: 'is', # is not
  420. pre_condition: 'current_user.id',
  421. },
  422. 'ticket.owner_id' => {
  423. operator: 'is', # is not
  424. pre_condition: 'specific',
  425. value: 4711,
  426. },
  427. 'ticket.escalation_at' => {
  428. operator: 'is not', # not
  429. value: nil,
  430. },
  431. 'ticket.tags' => {
  432. operator: 'contains all', # contains all|contains one|contains all not|contains one not
  433. value: 'tag1, tag2',
  434. },
  435. }
  436. =end
  437. def self.selector2sql(selectors, options = {})
  438. current_user = options[:current_user]
  439. current_user_id = UserInfo.current_user_id
  440. if current_user
  441. current_user_id = current_user.id
  442. end
  443. return if !selectors
  444. # remember query and bind params
  445. query = ''
  446. bind_params = []
  447. like = Rails.application.config.db_like
  448. if selectors.respond_to?(:permit!)
  449. selectors = selectors.permit!.to_h
  450. end
  451. # get tables to join
  452. tables = ''
  453. selectors.each do |attribute, selector_raw|
  454. attributes = attribute.split('.')
  455. selector = selector_raw.stringify_keys
  456. next if !attributes[1]
  457. next if attributes[0] == 'execution_time'
  458. next if tables.include?(attributes[0])
  459. next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
  460. next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
  461. if query != ''
  462. query += ' AND '
  463. end
  464. case attributes[0]
  465. when 'customer'
  466. tables += ', users customers'
  467. query += 'tickets.customer_id = customers.id'
  468. when 'organization'
  469. tables += ', organizations'
  470. query += 'tickets.organization_id = organizations.id'
  471. when 'owner'
  472. tables += ', users owners'
  473. query += 'tickets.owner_id = owners.id'
  474. when 'article'
  475. tables += ', ticket_articles articles'
  476. query += 'tickets.id = articles.ticket_id'
  477. when 'ticket_state'
  478. tables += ', ticket_states'
  479. query += 'tickets.state_id = ticket_states.id'
  480. when 'ticket'
  481. if attributes[1] == 'mention_user_ids'
  482. tables += ', mentions'
  483. query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
  484. end
  485. else
  486. raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
  487. end
  488. end
  489. # add conditions
  490. no_result = false
  491. selectors.each do |attribute, selector_raw|
  492. # validation
  493. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
  494. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
  495. selector = selector_raw.stringify_keys
  496. raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
  497. raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(%r{^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before|till|from)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$})
  498. # validate value / allow blank but only if pre_condition exists and is not specific
  499. if !selector.key?('value') ||
  500. (selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
  501. (selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
  502. return nil if selector['pre_condition'].nil?
  503. return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
  504. return nil if selector['pre_condition'] == 'specific'
  505. end
  506. # validate pre_condition values
  507. return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)}
  508. # get attributes
  509. attributes = attribute.split('.')
  510. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}"
  511. # magic selectors
  512. if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id'
  513. attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
  514. end
  515. if attributes[0] == 'ticket' && attributes[1] == 'tags'
  516. selector['value'] = selector['value'].split(',').collect(&:strip)
  517. end
  518. if selector['operator'].include?('in working time')
  519. next if attributes[1] != 'calendar_id'
  520. raise __('Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)') if !options[:execution_time]
  521. biz = Calendar.lookup(id: selector['value'])&.biz
  522. next if biz.blank?
  523. if (selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now)) || (selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now))
  524. no_result = true
  525. break
  526. end
  527. # skip to next condition
  528. next
  529. end
  530. if query != ''
  531. query += ' AND '
  532. end
  533. # because of no grouping support we select not_set by sub select for mentions
  534. if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
  535. if selector['pre_condition'] == 'not_set'
  536. query += if selector['operator'] == 'is'
  537. "(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
  538. else
  539. "1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
  540. end
  541. else
  542. query += if selector['operator'] == 'is'
  543. 'mentions.user_id IN (?)'
  544. else
  545. 'mentions.user_id NOT IN (?)'
  546. end
  547. if selector['pre_condition'] == 'current_user.id'
  548. bind_params.push current_user_id
  549. else
  550. bind_params.push selector['value']
  551. end
  552. end
  553. next
  554. end
  555. if selector['operator'] == 'is'
  556. if selector['pre_condition'] == 'not_set'
  557. if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  558. query += "(#{attribute} IS NULL OR #{attribute} IN (?))"
  559. bind_params.push 1
  560. else
  561. query += "#{attribute} IS NULL"
  562. end
  563. elsif selector['pre_condition'] == 'current_user.id'
  564. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  565. query += "#{attribute} IN (?)"
  566. if attributes[1] == 'out_of_office_replacement_id'
  567. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  568. else
  569. bind_params.push current_user_id
  570. end
  571. elsif selector['pre_condition'] == 'current_user.organization_id'
  572. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  573. query += "#{attribute} IN (?)"
  574. user = User.find_by(id: current_user_id)
  575. bind_params.push user.all_organization_ids
  576. else
  577. # rubocop:disable Style/IfInsideElse
  578. if selector['value'].nil?
  579. query += "#{attribute} IS NULL"
  580. else
  581. if attributes[1] == 'out_of_office_replacement_id'
  582. query += "#{attribute} IN (?)"
  583. bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
  584. else
  585. if selector['value'].class != Array
  586. selector['value'] = [selector['value']]
  587. end
  588. query += if selector['value'].include?('')
  589. "(#{attribute} IN (?) OR #{attribute} IS NULL)"
  590. else
  591. "#{attribute} IN (?)"
  592. end
  593. bind_params.push selector['value']
  594. end
  595. end
  596. # rubocop:enable Style/IfInsideElse
  597. end
  598. elsif selector['operator'] == 'is not'
  599. if selector['pre_condition'] == 'not_set'
  600. if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
  601. query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  602. bind_params.push 1
  603. else
  604. query += "#{attribute} IS NOT NULL"
  605. end
  606. elsif selector['pre_condition'] == 'current_user.id'
  607. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  608. if attributes[1] == 'out_of_office_replacement_id'
  609. bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
  610. else
  611. bind_params.push current_user_id
  612. end
  613. elsif selector['pre_condition'] == 'current_user.organization_id'
  614. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  615. user = User.find_by(id: current_user_id)
  616. bind_params.push user.organization_id
  617. else
  618. # rubocop:disable Style/IfInsideElse
  619. if selector['value'].nil?
  620. query += "#{attribute} IS NOT NULL"
  621. else
  622. if attributes[1] == 'out_of_office_replacement_id'
  623. bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
  624. query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  625. else
  626. if selector['value'].class != Array
  627. selector['value'] = [selector['value']]
  628. end
  629. query += if selector['value'].include?('')
  630. "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
  631. else
  632. "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
  633. end
  634. bind_params.push selector['value']
  635. end
  636. end
  637. # rubocop:enable Style/IfInsideElse
  638. end
  639. elsif selector['operator'] == 'contains'
  640. query += "#{attribute} #{like} (?)"
  641. value = "%#{selector['value']}%"
  642. bind_params.push value
  643. elsif selector['operator'] == 'contains not'
  644. query += "#{attribute} NOT #{like} (?)"
  645. value = "%#{selector['value']}%"
  646. bind_params.push value
  647. elsif selector['operator'] == 'contains all'
  648. if attributes[0] == 'ticket' && attributes[1] == 'tags'
  649. query += "? = (
  650. SELECT
  651. COUNT(*)
  652. FROM
  653. tag_objects,
  654. tag_items,
  655. tags
  656. WHERE
  657. tickets.id = tags.o_id AND
  658. tag_objects.id = tags.tag_object_id AND
  659. tag_objects.name = 'Ticket' AND
  660. tag_items.id = tags.tag_item_id AND
  661. tag_items.name IN (?)
  662. )"
  663. bind_params.push selector['value'].count
  664. bind_params.push selector['value']
  665. elsif Ticket.column_names.include?(attributes[1])
  666. query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'])
  667. end
  668. elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket'
  669. if attributes[1] == 'tags'
  670. tables += ', tag_objects, tag_items, tags'
  671. query += "
  672. tickets.id = tags.o_id AND
  673. tag_objects.id = tags.tag_object_id AND
  674. tag_objects.name = 'Ticket' AND
  675. tag_items.id = tags.tag_item_id AND
  676. tag_items.name IN (?)"
  677. bind_params.push selector['value']
  678. elsif Ticket.column_names.include?(attributes[1])
  679. query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'])
  680. end
  681. elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket'
  682. if attributes[1] == 'tags'
  683. query += "0 = (
  684. SELECT
  685. COUNT(*)
  686. FROM
  687. tag_objects,
  688. tag_items,
  689. tags
  690. WHERE
  691. tickets.id = tags.o_id AND
  692. tag_objects.id = tags.tag_object_id AND
  693. tag_objects.name = 'Ticket' AND
  694. tag_items.id = tags.tag_item_id AND
  695. tag_items.name IN (?)
  696. )"
  697. bind_params.push selector['value']
  698. elsif Ticket.column_names.include?(attributes[1])
  699. query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'], negated: true)
  700. end
  701. elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket'
  702. if attributes[1] == 'tags'
  703. query += "(
  704. SELECT
  705. COUNT(*)
  706. FROM
  707. tag_objects,
  708. tag_items,
  709. tags
  710. WHERE
  711. tickets.id = tags.o_id AND
  712. tag_objects.id = tags.tag_object_id AND
  713. tag_objects.name = 'Ticket' AND
  714. tag_items.id = tags.tag_item_id AND
  715. tag_items.name IN (?)
  716. ) BETWEEN 0 AND 0"
  717. bind_params.push selector['value']
  718. elsif Ticket.column_names.include?(attributes[1])
  719. query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'], negated: true)
  720. end
  721. elsif selector['operator'] == 'before (absolute)'
  722. query += "#{attribute} <= ?"
  723. bind_params.push selector['value']
  724. elsif selector['operator'] == 'after (absolute)'
  725. query += "#{attribute} >= ?"
  726. bind_params.push selector['value']
  727. elsif selector['operator'] == 'within last (relative)'
  728. query += "#{attribute} BETWEEN ? AND ?"
  729. time = range(selector).ago
  730. bind_params.push time
  731. bind_params.push Time.zone.now
  732. elsif selector['operator'] == 'within next (relative)'
  733. query += "#{attribute} BETWEEN ? AND ?"
  734. time = range(selector).from_now
  735. bind_params.push Time.zone.now
  736. bind_params.push time
  737. elsif selector['operator'] == 'before (relative)'
  738. query += "#{attribute} <= ?"
  739. time = range(selector).ago
  740. bind_params.push time
  741. elsif selector['operator'] == 'after (relative)'
  742. query += "#{attribute} >= ?"
  743. time = range(selector).from_now
  744. bind_params.push time
  745. elsif selector['operator'] == 'till (relative)'
  746. query += "#{attribute} <= ?"
  747. time = range(selector).from_now
  748. bind_params.push time
  749. elsif selector['operator'] == 'from (relative)'
  750. query += "#{attribute} >= ?"
  751. time = range(selector).ago
  752. bind_params.push time
  753. else
  754. raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
  755. end
  756. end
  757. return if no_result
  758. [query, bind_params, tables]
  759. end
  760. =begin
  761. perform changes on ticket
  762. ticket.perform_changes(trigger, 'trigger', item, current_user_id)
  763. # or
  764. ticket.perform_changes(job, 'job', item, current_user_id)
  765. =end
  766. def perform_changes(performable, perform_origin, item = nil, current_user_id = nil)
  767. perform = performable.perform
  768. logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }
  769. article = begin
  770. Ticket::Article.find_by(id: item.try(:dig, :article_id))
  771. rescue ArgumentError
  772. nil
  773. end
  774. # if the configuration contains the deletion of the ticket then
  775. # we skip all other ticket changes because they does not matter
  776. if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
  777. perform.each_key do |key|
  778. (object_name, attribute) = key.split('.', 2)
  779. next if object_name != 'ticket'
  780. next if attribute == 'action'
  781. perform.delete(key)
  782. end
  783. end
  784. objects = build_notification_template_objects(article)
  785. perform_notification = {}
  786. perform_article = {}
  787. changed = false
  788. perform.each do |key, value|
  789. (object_name, attribute) = key.split('.', 2)
  790. 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'
  791. # send notification/create article (after changes are done)
  792. if object_name == 'article'
  793. perform_article[key] = value
  794. next
  795. end
  796. if object_name == 'notification'
  797. perform_notification[key] = value
  798. next
  799. end
  800. # Apply pending_time changes
  801. if key == 'ticket.pending_time'
  802. new_value = case value['operator']
  803. when 'static'
  804. value['value']
  805. when 'relative'
  806. TimeRangeHelper.relative(range: value['range'], value: value['value'])
  807. end
  808. if new_value
  809. self[attribute] = new_value
  810. changed = true
  811. next
  812. end
  813. end
  814. # update tags
  815. if key == 'ticket.tags'
  816. next if value['value'].blank?
  817. tags = value['value'].split(',')
  818. case value['operator']
  819. when 'add'
  820. tags.each do |tag|
  821. tag_add(tag, current_user_id || 1)
  822. end
  823. when 'remove'
  824. tags.each do |tag|
  825. tag_remove(tag, current_user_id || 1)
  826. end
  827. else
  828. logger.error "Unknown #{attribute} operator #{value['operator']}"
  829. end
  830. next
  831. end
  832. # delete ticket
  833. if key == 'ticket.action'
  834. next if value['value'].blank?
  835. next if value['value'] != 'delete'
  836. logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
  837. destroy!
  838. next
  839. end
  840. # lookup pre_condition
  841. if value['pre_condition']
  842. if value['pre_condition'].start_with?('not_set')
  843. value['value'] = 1
  844. elsif value['pre_condition'].start_with?('current_user.')
  845. raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
  846. value['value'] = current_user_id
  847. end
  848. end
  849. # update ticket
  850. next if self[attribute].to_s == value['value'].to_s
  851. changed = true
  852. if value['value'].is_a?(String)
  853. value['value'] = NotificationFactory::Mailer.template(
  854. templateInline: value['value'],
  855. objects: objects,
  856. quote: true,
  857. )
  858. end
  859. self[attribute] = value['value']
  860. logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
  861. end
  862. if changed
  863. save!
  864. end
  865. perform_article.each do |key, value|
  866. raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
  867. add_trigger_note(id, value, objects, perform_origin)
  868. end
  869. perform_notification.each do |key, value|
  870. # send notification
  871. case key
  872. when 'notification.sms'
  873. send_sms_notification(value, article, perform_origin)
  874. next
  875. when 'notification.email'
  876. send_email_notification(value, article, perform_origin)
  877. when 'notification.webhook'
  878. TriggerWebhookJob.perform_later(performable, self, article)
  879. end
  880. end
  881. true
  882. end
  883. =begin
  884. perform changes on ticket
  885. ticket.add_trigger_note(ticket_id, note, objects, perform_origin)
  886. =end
  887. def add_trigger_note(ticket_id, note, objects, perform_origin)
  888. rendered_subject = NotificationFactory::Mailer.template(
  889. templateInline: note[:subject],
  890. objects: objects,
  891. quote: true,
  892. )
  893. rendered_body = NotificationFactory::Mailer.template(
  894. templateInline: note[:body],
  895. objects: objects,
  896. quote: true,
  897. )
  898. Ticket::Article.create!(
  899. ticket_id: ticket_id,
  900. subject: rendered_subject,
  901. content_type: 'text/html',
  902. body: rendered_body,
  903. internal: note[:internal],
  904. sender: Ticket::Article::Sender.find_by(name: 'System'),
  905. type: Ticket::Article::Type.find_by(name: 'note'),
  906. preferences: {
  907. perform_origin: perform_origin,
  908. notification: true,
  909. },
  910. updated_by_id: 1,
  911. created_by_id: 1,
  912. )
  913. end
  914. =begin
  915. perform active triggers on ticket
  916. Ticket.perform_triggers(ticket, article, item, options)
  917. =end
  918. def self.perform_triggers(ticket, article, item, options = {})
  919. recursive = Setting.get('ticket_trigger_recursive')
  920. type = options[:type] || item[:type]
  921. local_options = options.clone
  922. local_options[:type] = type
  923. local_options[:reset_user_id] = true
  924. local_options[:disable] = ['Transaction::Notification']
  925. local_options[:trigger_ids] ||= {}
  926. local_options[:trigger_ids][ticket.id.to_s] ||= []
  927. local_options[:loop_count] ||= 0
  928. local_options[:loop_count] += 1
  929. ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
  930. if local_options[:loop_count] > ticket_trigger_recursive_max_loop
  931. message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
  932. logger.info { message }
  933. return [false, message]
  934. end
  935. triggers = if Rails.configuration.db_case_sensitive
  936. ::Trigger.where(active: true).order(Arel.sql('LOWER(name)'))
  937. else
  938. ::Trigger.where(active: true).order(:name)
  939. end
  940. return [true, __('No triggers active')] if triggers.blank?
  941. # check if notification should be send because of customer emails
  942. send_notification = true
  943. if local_options[:send_notification] == false
  944. send_notification = false
  945. elsif item[:article_id]
  946. article = Ticket::Article.lookup(id: item[:article_id])
  947. if article&.preferences && article.preferences['send-auto-response'] == false
  948. send_notification = false
  949. end
  950. end
  951. Transaction.execute(local_options) do
  952. triggers.each do |trigger|
  953. logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  954. condition = trigger.condition
  955. # check if one article attribute is used
  956. one_has_changed_done = false
  957. article_selector = false
  958. trigger.condition.each_key do |key|
  959. (object_name, attribute) = key.split('.', 2)
  960. next if object_name != 'article'
  961. next if attribute == 'id'
  962. article_selector = true
  963. end
  964. if article && article_selector
  965. one_has_changed_done = true
  966. end
  967. if article && type == 'update'
  968. one_has_changed_done = true
  969. end
  970. # check ticket "has changed" options
  971. has_changed_done = true
  972. condition.each do |key, value|
  973. next if value.blank?
  974. next if value['operator'].blank?
  975. next if !value['operator']['has changed']
  976. # remove condition item, because it has changed
  977. (object_name, attribute) = key.split('.', 2)
  978. next if object_name != 'ticket'
  979. next if item[:changes].blank?
  980. next if !item[:changes].key?(attribute)
  981. condition.delete(key)
  982. one_has_changed_done = true
  983. end
  984. # check if we have not matching "has changed" attributes
  985. condition.each_value do |value|
  986. next if value.blank?
  987. next if value['operator'].blank?
  988. next if !value['operator']['has changed']
  989. has_changed_done = false
  990. break
  991. end
  992. # check ticket action
  993. if condition['ticket.action']
  994. next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type
  995. next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type
  996. condition.delete('ticket.action')
  997. end
  998. next if !has_changed_done
  999. # check in min one attribute of condition has changed on update
  1000. one_has_changed_condition = false
  1001. if type == 'update'
  1002. # verify if ticket condition exists
  1003. condition.each_key do |key|
  1004. (object_name, attribute) = key.split('.', 2)
  1005. next if object_name != 'ticket'
  1006. one_has_changed_condition = true
  1007. next if item[:changes].blank?
  1008. next if !item[:changes].key?(attribute)
  1009. one_has_changed_done = true
  1010. break
  1011. end
  1012. next if one_has_changed_condition && !one_has_changed_done
  1013. end
  1014. # check if ticket selector is matching
  1015. condition['ticket.id'] = {
  1016. operator: 'is',
  1017. value: ticket.id,
  1018. }
  1019. next if article_selector && !article
  1020. # check if article selector is matching
  1021. if article_selector
  1022. condition['article.id'] = {
  1023. operator: 'is',
  1024. value: article.id,
  1025. }
  1026. end
  1027. user_id = ticket.updated_by_id
  1028. if article
  1029. user_id = article.updated_by_id
  1030. end
  1031. user = User.lookup(id: user_id)
  1032. # verify is condition is matching
  1033. ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore')
  1034. next if ticket_count.blank?
  1035. next if ticket_count.zero?
  1036. next if tickets.first.id != ticket.id
  1037. if recursive == false && local_options[:loop_count] > 1
  1038. 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."
  1039. logger.info { message }
  1040. return [true, message]
  1041. end
  1042. if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
  1043. recipient = trigger.perform['notification.email']['recipient']
  1044. local_options[:send_notification] = false
  1045. if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
  1046. 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})" }
  1047. next
  1048. end
  1049. end
  1050. if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
  1051. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  1052. next
  1053. end
  1054. local_options[:trigger_ids][ticket.id.to_s].push trigger.id
  1055. logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  1056. ticket.perform_changes(trigger, 'trigger', item, user_id)
  1057. if recursive == true
  1058. TransactionDispatcher.commit(local_options)
  1059. end
  1060. end
  1061. end
  1062. [true, ticket, local_options]
  1063. end
  1064. def self.range(selector)
  1065. selector['value'].to_i.send(selector['range'].pluralize)
  1066. rescue
  1067. raise 'unknown selector'
  1068. end
  1069. =begin
  1070. get all email references headers of a ticket, to exclude some, parse it as array into method
  1071. references = ticket.get_references
  1072. result
  1073. ['message-id-1234', 'message-id-5678']
  1074. ignore references header(s)
  1075. references = ticket.get_references(['message-id-5678'])
  1076. result
  1077. ['message-id-1234']
  1078. =end
  1079. def get_references(ignore = [])
  1080. references = []
  1081. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
  1082. if article.in_reply_to.present?
  1083. references.push article.in_reply_to
  1084. end
  1085. next if article.message_id.blank?
  1086. references.push article.message_id
  1087. end
  1088. ignore.each do |item|
  1089. references.delete(item)
  1090. end
  1091. references
  1092. end
  1093. =begin
  1094. get all articles of a ticket in correct order (overwrite active record default method)
  1095. articles = ticket.articles
  1096. result
  1097. [article1, article2]
  1098. =end
  1099. def articles
  1100. Ticket::Article.where(ticket_id: id).order(:created_at, :id)
  1101. end
  1102. # Get whichever #last_contact_* was later
  1103. # This is not identical to #last_contact_at
  1104. # It returns time to last original (versus follow up) contact
  1105. # @return [Time, nil]
  1106. def last_original_update_at
  1107. [last_contact_agent_at, last_contact_customer_at].compact.max
  1108. end
  1109. # true if conversation did happen and agent responded
  1110. # false if customer is waiting for response or agent reached out and customer did not respond yet
  1111. # @return [Bool]
  1112. def agent_responded?
  1113. return false if last_contact_customer_at.blank?
  1114. return false if last_contact_agent_at.blank?
  1115. last_contact_customer_at < last_contact_agent_at
  1116. end
  1117. private
  1118. def check_generate
  1119. return true if number
  1120. self.number = Ticket::Number.generate
  1121. true
  1122. end
  1123. def check_title
  1124. return true if !title
  1125. title.gsub!(%r{\s|\t|\r}, ' ')
  1126. true
  1127. end
  1128. def check_defaults
  1129. if !owner_id
  1130. self.owner_id = 1
  1131. end
  1132. return true if !customer_id
  1133. customer = User.find_by(id: customer_id)
  1134. return true if !customer
  1135. return true if organization_id.present? && customer.organization_id?(organization_id)
  1136. self.organization_id = customer.organization_id
  1137. true
  1138. end
  1139. def reset_pending_time
  1140. # ignore if no state has changed
  1141. return true if !changes_to_save['state_id']
  1142. # ignore if new state is blank and
  1143. # let handle ActiveRecord the error
  1144. return if state_id.blank?
  1145. # check if new state isn't pending*
  1146. current_state = Ticket::State.lookup(id: state_id)
  1147. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  1148. # in case, set pending_time to nil
  1149. return true if current_state_type.name.match?(%r{^pending}i)
  1150. self.pending_time = nil
  1151. true
  1152. end
  1153. def set_default_state
  1154. return true if state_id
  1155. default_ticket_state = Ticket::State.find_by(default_create: true)
  1156. return true if !default_ticket_state
  1157. self.state_id = default_ticket_state.id
  1158. true
  1159. end
  1160. def set_default_priority
  1161. return true if priority_id
  1162. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  1163. return true if !default_ticket_priority
  1164. self.priority_id = default_ticket_priority.id
  1165. true
  1166. end
  1167. def check_owner_active
  1168. return true if Setting.get('import_mode')
  1169. # only change the owner for non closed Tickets for historical/reporting reasons
  1170. return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
  1171. # return when ticket is unassigned
  1172. return true if owner_id.blank?
  1173. return true if owner_id == 1
  1174. # return if owner is active, is agent and has access to group of ticket
  1175. return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
  1176. # else set the owner of the ticket to the default user as unassigned
  1177. self.owner_id = 1
  1178. true
  1179. end
  1180. # articles.last breaks (returns the wrong article)
  1181. # if another email notification trigger preceded this one
  1182. # (see https://github.com/zammad/zammad/issues/1543)
  1183. def build_notification_template_objects(article)
  1184. {
  1185. ticket: self,
  1186. article: article || articles.last
  1187. }
  1188. end
  1189. def send_email_notification(value, article, perform_origin)
  1190. # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
  1191. value_recipient = Array(value['recipient'])
  1192. recipients_raw = []
  1193. value_recipient.each do |recipient|
  1194. case recipient
  1195. when 'article_last_sender'
  1196. if article.present?
  1197. if article.reply_to.present?
  1198. recipients_raw.push(article.reply_to)
  1199. elsif article.from.present?
  1200. recipients_raw.push(article.from)
  1201. elsif article.origin_by_id
  1202. email = User.find_by(id: article.origin_by_id).email
  1203. recipients_raw.push(email)
  1204. elsif article.created_by_id
  1205. email = User.find_by(id: article.created_by_id).email
  1206. recipients_raw.push(email)
  1207. end
  1208. end
  1209. when 'ticket_customer'
  1210. email = User.find_by(id: customer_id).email
  1211. recipients_raw.push(email)
  1212. when 'ticket_owner'
  1213. email = User.find_by(id: owner_id).email
  1214. recipients_raw.push(email)
  1215. when 'ticket_agents'
  1216. User.group_access(group_id, 'full').sort_by(&:login).each do |user|
  1217. recipients_raw.push(user.email)
  1218. end
  1219. when %r{\Auserid_(\d+)\z}
  1220. user = User.lookup(id: $1)
  1221. if !user
  1222. logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
  1223. next
  1224. end
  1225. recipients_raw.push(user.email)
  1226. else
  1227. logger.error "Unknown email notification recipient '#{recipient}'"
  1228. next
  1229. end
  1230. end
  1231. recipients_checked = []
  1232. recipients_raw.each do |recipient_email|
  1233. users = User.where(email: recipient_email)
  1234. next if users.any? { |user| !trigger_based_notification?(user) }
  1235. # send notifications only to email addresses
  1236. next if recipient_email.blank?
  1237. # check if address is valid
  1238. begin
  1239. Mail::AddressList.new(recipient_email).addresses.each do |address|
  1240. recipient_email = address.address
  1241. email_address_validation = EmailAddressValidation.new(recipient_email)
  1242. break if recipient_email.present? && email_address_validation.valid?
  1243. end
  1244. rescue
  1245. if recipient_email.present?
  1246. if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
  1247. next # no usable format found
  1248. end
  1249. recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
  1250. end
  1251. end
  1252. email_address_validation = EmailAddressValidation.new(recipient_email)
  1253. next if !email_address_validation.valid?
  1254. # do not send notification if system address
  1255. next if EmailAddress.exists?(email: recipient_email.downcase)
  1256. # do not sent notifications to this recipients
  1257. send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
  1258. begin
  1259. next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
  1260. rescue => e
  1261. logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
  1262. logger.error e
  1263. next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
  1264. end
  1265. # check if notification should be send because of customer emails
  1266. if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
  1267. logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
  1268. next
  1269. end
  1270. # loop protection / check if maximal count of trigger mail has reached
  1271. map = {
  1272. 10 => 10,
  1273. 30 => 15,
  1274. 60 => 25,
  1275. 180 => 50,
  1276. 600 => 100,
  1277. }
  1278. skip = false
  1279. map.each do |minutes, count|
  1280. already_sent = Ticket::Article.where(
  1281. ticket_id: id,
  1282. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1283. type: Ticket::Article::Type.find_by(name: 'email'),
  1284. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  1285. next if already_sent < count
  1286. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
  1287. skip = true
  1288. break
  1289. end
  1290. next if skip
  1291. map = {
  1292. 10 => 30,
  1293. 30 => 60,
  1294. 60 => 120,
  1295. 180 => 240,
  1296. 600 => 360,
  1297. }
  1298. skip = false
  1299. map.each do |minutes, count|
  1300. already_sent = Ticket::Article.where(
  1301. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1302. type: Ticket::Article::Type.find_by(name: 'email'),
  1303. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
  1304. next if already_sent < count
  1305. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
  1306. skip = true
  1307. break
  1308. end
  1309. next if skip
  1310. email = recipient_email.downcase.strip
  1311. next if recipients_checked.include?(email)
  1312. recipients_checked.push(email)
  1313. end
  1314. return if recipients_checked.blank?
  1315. recipient_string = recipients_checked.join(', ')
  1316. group_id = self.group_id
  1317. return if !group_id
  1318. email_address = Group.find(group_id).email_address
  1319. if !email_address
  1320. logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
  1321. return
  1322. end
  1323. if !email_address.channel_id
  1324. 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})"
  1325. return
  1326. end
  1327. security = nil
  1328. if Setting.get('smime_integration')
  1329. sign = value['sign'].present? && value['sign'] != 'no'
  1330. encryption = value['encryption'].present? && value['encryption'] != 'no'
  1331. security = {
  1332. type: 'S/MIME',
  1333. sign: {
  1334. success: false,
  1335. },
  1336. encryption: {
  1337. success: false,
  1338. },
  1339. }
  1340. if sign
  1341. sign_found = false
  1342. begin
  1343. list = Mail::AddressList.new(email_address.email)
  1344. from = list.addresses.first.to_s
  1345. cert = SMIMECertificate.for_sender_email_address(from)
  1346. if cert && !cert.expired?
  1347. sign_found = true
  1348. security[:sign][:success] = true
  1349. security[:sign][:comment] = "certificate for #{email_address.email} found"
  1350. end
  1351. rescue # rubocop:disable Lint/SuppressedException
  1352. end
  1353. if value['sign'] == 'discard' && !sign_found
  1354. 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)."
  1355. return
  1356. end
  1357. end
  1358. if encryption
  1359. certs_found = false
  1360. begin
  1361. SMIMECertificate.for_recipipent_email_addresses!(recipients_checked)
  1362. certs_found = true
  1363. security[:encryption][:success] = true
  1364. security[:encryption][:comment] = "certificates found for #{recipient_string}"
  1365. rescue # rubocop:disable Lint/SuppressedException
  1366. end
  1367. if value['encryption'] == 'discard' && !certs_found
  1368. logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
  1369. return
  1370. end
  1371. end
  1372. end
  1373. objects = build_notification_template_objects(article)
  1374. # get subject
  1375. subject = NotificationFactory::Mailer.template(
  1376. templateInline: value['subject'],
  1377. objects: objects,
  1378. quote: false,
  1379. )
  1380. subject = subject_build(subject)
  1381. body = NotificationFactory::Mailer.template(
  1382. templateInline: value['body'],
  1383. objects: objects,
  1384. quote: true,
  1385. )
  1386. (body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
  1387. preferences = {}
  1388. preferences[:perform_origin] = perform_origin
  1389. if security.present?
  1390. preferences[:security] = security
  1391. end
  1392. message = Ticket::Article.create(
  1393. ticket_id: id,
  1394. to: recipient_string,
  1395. subject: subject,
  1396. content_type: 'text/html',
  1397. body: body,
  1398. internal: value['internal'] || false, # default to public if value was not set
  1399. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1400. type: Ticket::Article::Type.find_by(name: 'email'),
  1401. preferences: preferences,
  1402. updated_by_id: 1,
  1403. created_by_id: 1,
  1404. )
  1405. attachments_inline.each do |attachment|
  1406. Store.create!(
  1407. object: 'Ticket::Article',
  1408. o_id: message.id,
  1409. data: attachment[:data],
  1410. filename: attachment[:filename],
  1411. preferences: attachment[:preferences],
  1412. )
  1413. end
  1414. original_article = objects[:article]
  1415. if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
  1416. original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
  1417. end
  1418. if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
  1419. original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
  1420. original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
  1421. end
  1422. end
  1423. def sms_recipients_by_type(recipient_type, article)
  1424. case recipient_type
  1425. when 'article_last_sender'
  1426. return nil if article.blank?
  1427. if article.origin_by_id
  1428. article.origin_by_id
  1429. elsif article.created_by_id
  1430. article.created_by_id
  1431. end
  1432. when 'ticket_customer'
  1433. customer_id
  1434. when 'ticket_owner'
  1435. owner_id
  1436. when 'ticket_agents'
  1437. User.group_access(group_id, 'full').sort_by(&:login)
  1438. when %r{\Auserid_(\d+)\z}
  1439. return $1 if User.exists?($1)
  1440. logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
  1441. nil
  1442. else
  1443. logger.error "Unknown sms notification recipient '#{recipient}'"
  1444. nil
  1445. end
  1446. end
  1447. def build_sms_recipients_list(value, article)
  1448. Array(value['recipient'])
  1449. .each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
  1450. .map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
  1451. .uniq(&:id)
  1452. .select { |user| user.mobile.present? }
  1453. end
  1454. def send_sms_notification(value, article, perform_origin)
  1455. sms_recipients = build_sms_recipients_list(value, article)
  1456. if sms_recipients.blank?
  1457. logger.debug "No SMS recipients found for Ticket# #{number}"
  1458. return
  1459. end
  1460. sms_recipients_to = sms_recipients
  1461. .map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
  1462. .join(', ')
  1463. channel = Channel.find_by(area: 'Sms::Notification')
  1464. if !channel.active?
  1465. # write info message since we have an active trigger
  1466. logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
  1467. return
  1468. end
  1469. objects = build_notification_template_objects(article)
  1470. body = NotificationFactory::Renderer.new(
  1471. objects: objects,
  1472. template: value['body'],
  1473. escape: false
  1474. ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space
  1475. # attributes content_type is not needed for SMS
  1476. Ticket::Article.create(
  1477. ticket_id: id,
  1478. subject: 'SMS notification',
  1479. to: sms_recipients_to,
  1480. body: body,
  1481. internal: value['internal'] || false, # default to public if value was not set
  1482. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1483. type: Ticket::Article::Type.find_by(name: 'sms'),
  1484. preferences: {
  1485. perform_origin: perform_origin,
  1486. sms_recipients: sms_recipients.map(&:mobile),
  1487. channel_id: channel.id,
  1488. },
  1489. updated_by_id: 1,
  1490. created_by_id: 1,
  1491. )
  1492. end
  1493. def trigger_based_notification?(user)
  1494. blocked_in_days = trigger_based_notification_blocked_in_days(user)
  1495. return true if blocked_in_days.zero?
  1496. logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
  1497. false
  1498. end
  1499. def trigger_based_notification_blocked_in_days(user)
  1500. return 0 if !user.preferences[:mail_delivery_failed]
  1501. return 0 if user.preferences[:mail_delivery_failed_data].blank?
  1502. # blocked for 60 full days
  1503. (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  1504. end
  1505. end