microsoft_graph.rb 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class MicrosoftGraph
  3. BASE_URL = 'https://graph.microsoft.com/v1.0/'.freeze
  4. attr_reader :bearer_token, :mailbox
  5. def initialize(access_token:, mailbox:)
  6. super()
  7. @bearer_token = access_token
  8. @mailbox = mailbox
  9. end
  10. def send_message(mail)
  11. headers = { 'Content-Type' => 'text/plain' }
  12. encoded = Base64.strict_encode64(mail.to_s)
  13. options = { headers:, send_as_raw_body: encoded }
  14. make_request('sendMail', method: :post, json: false, options:)
  15. end
  16. def store_mocked_message(params, folder_id: 'inbox')
  17. make_request("mailFolders/#{folder_id}/messages", method: :post, params:)
  18. end
  19. def list_messages(unread_only: false, per_page: 1000, follow_pagination: true, folder_id: nil, select: 'id')
  20. path = 'messages/?$count=true&$orderby=receivedDateTime ASC'
  21. path += "&$select=#{select}"
  22. path += "&top=#{per_page}"
  23. filters = []
  24. filters << 'isRead eq false' if unread_only
  25. filters << "parentFolderId eq '#{folder_id || 'inbox'}'"
  26. if filters.any?
  27. path += "&$filter=(#{filters.join(' AND ')})"
  28. end
  29. make_paginated_request(path, follow_pagination:)
  30. end
  31. def create_message_folder(name, parent_folder_id: nil)
  32. path = if parent_folder_id.present?
  33. "mailFolders/#{parent_folder_id}/childFolders"
  34. else
  35. 'mailFolders'
  36. end
  37. params = { displayName: name }
  38. make_request(path, method: :post, params:)
  39. end
  40. def get_message_folder_details(id)
  41. make_request("mailFolders/#{id}")
  42. end
  43. def delete_message_folder(id)
  44. make_request("mailFolders/#{id}", method: :delete)
  45. end
  46. def get_message_folders_tree(parent = nil)
  47. path = if parent.present?
  48. "mailFolders/#{parent}/childFolders"
  49. else
  50. 'mailFolders'
  51. end
  52. make_paginated_request("#{path}?$expand=childFolders($top=9999)&$top=9999")
  53. .fetch(:items, [])
  54. .map do |elem|
  55. {
  56. id: elem[:id],
  57. displayName: elem[:displayName],
  58. childFolders: elem[:childFolders].map do |expanded_elem|
  59. if expanded_elem[:childFolderCount].positive?
  60. expanded_folders = get_message_folders_tree(expanded_elem[:id])
  61. end
  62. {
  63. id: expanded_elem[:id],
  64. displayName: expanded_elem[:displayName],
  65. childFolders: expanded_folders || []
  66. }
  67. end
  68. }
  69. end
  70. end
  71. def get_message_basic_details(message_id)
  72. result = make_request("messages/#{message_id}/?$expand=singleValueExtendedProperties($filter=Id%20eq%20'LONG%200x0E08')&$select=internetMessageHeaders")
  73. size = result.fetch(:singleValueExtendedProperties)
  74. .find { |elem| elem[:id] == 'Long 0xe08' }
  75. &.dig(:value)
  76. &.to_i
  77. headers = headers_to_hash result.fetch(:internetMessageHeaders, {})
  78. { headers:, size: }
  79. end
  80. def get_raw_message(message_id)
  81. make_request("messages/#{message_id}/$value", json: false)
  82. end
  83. def mark_message_as_read(message_id)
  84. make_request("messages/#{message_id}", method: :patch, params: { isRead: true })
  85. end
  86. def delete_message(message_id)
  87. make_request("messages/#{message_id}", method: :delete)
  88. end
  89. private
  90. def make_request(path, method: :get, json: true, params: {}, options: {})
  91. options[:bearer_token] = bearer_token
  92. options[:json] = json
  93. uri = URI(path).host.present? ? path : "#{BASE_URL}#{mailbox_path}#{path}"
  94. response = UserAgent.send(method, uri, params, options)
  95. if !response.success?
  96. error_details = if response&.body&.start_with?('{')
  97. JSON.parse(response.body)['error']
  98. else
  99. {
  100. code: response.code,
  101. message: response.body || response.error,
  102. }
  103. end
  104. raise ApiError, error_details
  105. end
  106. if json && (data = response.data.presence)
  107. return data.with_indifferent_access
  108. end
  109. response.body
  110. end
  111. PAGINATED_MAX_LOOPS = 25
  112. def make_paginated_request(path, follow_pagination: true, **)
  113. response = make_request(path, **)
  114. total_count = response[:'@odata.count']
  115. return { total_count:, items: response.fetch(:value) } if !follow_pagination
  116. received = [response]
  117. while (pagination_link = response['@odata.nextLink'])
  118. response = make_request(pagination_link)
  119. received << response
  120. if received.count > PAGINATED_MAX_LOOPS # rubocop:disable Style/Next
  121. error = {
  122. code: 'X-Zammad-MsGraphEndlessLoop',
  123. message: "Microsoft Graph API paginated response caused a permanenet loop: #{path}"
  124. }
  125. raise ApiError, error
  126. end
  127. end
  128. items = received.flat_map { |elem| elem.fetch(:value) }
  129. { total_count:, items: }
  130. end
  131. def headers_to_hash(input)
  132. input.each_with_object({}.with_indifferent_access) do |elem, memo|
  133. memo[elem[:name]] = elem[:value]
  134. end
  135. end
  136. def mailbox_path
  137. "users/#{mailbox}/"
  138. end
  139. end