microsoft_graph.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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/?$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. .map do |elem|
  54. {
  55. id: elem[:id],
  56. displayName: elem[:displayName],
  57. childFolders: elem[:childFolders].map do |expanded_elem|
  58. if expanded_elem[:childFolderCount].positive?
  59. expanded_folders = get_message_folders_tree(expanded_elem[:id])
  60. end
  61. {
  62. id: expanded_elem[:id],
  63. displayName: expanded_elem[:displayName],
  64. childFolders: expanded_folders || []
  65. }
  66. end
  67. }
  68. end
  69. end
  70. def get_message_basic_details(message_id)
  71. result = make_request("messages/#{message_id}/?$expand=singleValueExtendedProperties($filter=Id%20eq%20'LONG%200x0E08')&$select=internetMessageHeaders")
  72. size = result.fetch(:singleValueExtendedProperties)
  73. .find { |elem| elem[:id] == 'Long 0xe08' }
  74. &.dig(:value)
  75. &.to_i
  76. headers = headers_to_hash result.fetch(:internetMessageHeaders, {})
  77. { headers:, size: }
  78. end
  79. def get_raw_message(message_id)
  80. make_request("messages/#{message_id}/$value", json: false)
  81. end
  82. def mark_message_as_read(message_id)
  83. make_request("messages/#{message_id}", method: :patch, params: { isRead: true })
  84. end
  85. def delete_message(message_id)
  86. make_request("messages/#{message_id}", method: :delete)
  87. end
  88. private
  89. def make_request(path, method: :get, json: true, params: {}, options: {})
  90. options[:bearer_token] = bearer_token
  91. options[:json] = json
  92. uri = URI(path).host.present? ? path : "#{BASE_URL}#{mailbox_path}#{path}"
  93. response = UserAgent.send(method, uri, params, options)
  94. if !response.success?
  95. error_details = if response&.body&.start_with?('{')
  96. JSON.parse(response.body)['error']
  97. else
  98. {
  99. code: response.code,
  100. message: response.body || response.error,
  101. }
  102. end
  103. raise ApiError, error_details
  104. end
  105. if json && (data = response.data.presence)
  106. return data.with_indifferent_access
  107. end
  108. response.body
  109. end
  110. PAGINATED_MAX_LOOPS = 25
  111. def make_paginated_request(path, follow_pagination: true, **)
  112. response = make_request(path, **)
  113. return response.fetch(:value) if !follow_pagination
  114. received = [response]
  115. while (pagination_link = response['@odata.nextLink'])
  116. response = make_request(pagination_link)
  117. received << response
  118. if received.count > PAGINATED_MAX_LOOPS # rubocop:disable Style/Next
  119. error = {
  120. code: 'X-Zammad-MsGraphEndlessLoop',
  121. message: "Microsoft Graph API paginated response caused a permanenet loop: #{path}"
  122. }
  123. raise ApiError, error
  124. end
  125. end
  126. received.flat_map { |elem| elem.fetch(:value) }
  127. end
  128. def headers_to_hash(input)
  129. input.each_with_object({}.with_indifferent_access) do |elem, memo|
  130. memo[elem[:name]] = elem[:value]
  131. end
  132. end
  133. def mailbox_path
  134. "users/#{mailbox}/"
  135. end
  136. end