ticket.rb 55 KB

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