microsoft_graph_inbound.rb 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class Channel::Driver::MicrosoftGraphInbound < Channel::Driver::BaseEmailInbound
  3. =begin
  4. fetch emails from IMAP account
  5. instance = Channel::Driver::Imap.new
  6. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  7. returns
  8. {
  9. result: 'ok',
  10. fetched: 123,
  11. notice: 'e. g. message about to big emails in mailbox',
  12. }
  13. check if connect to IMAP account is possible, return count of mails in mailbox
  14. instance = Channel::Driver::Imap.new
  15. result = instance.fetch(params[:inbound][:options], channel, 'check')
  16. returns
  17. {
  18. result: 'ok',
  19. content_messages: 123,
  20. }
  21. verify IMAP account, check if search email is in there
  22. instance = Channel::Driver::Imap.new
  23. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  24. returns
  25. {
  26. result: 'ok', # 'verify not ok'
  27. }
  28. example
  29. params = {
  30. host: 'outlook.office365.com',
  31. user: 'xxx@zammad.onmicrosoft.com',
  32. password: 'xxx',
  33. keep_on_server: true,
  34. }
  35. OR
  36. params = {
  37. host: 'imap.gmail.com',
  38. user: 'xxx@gmail.com',
  39. password: 'xxx',
  40. keep_on_server: true,
  41. auth_type: 'XOAUTH2'
  42. }
  43. channel = Channel.last
  44. instance = Channel::Driver::Imap.new
  45. result = instance.fetch(params, channel, 'verify')
  46. =end
  47. def fetch(options, channel) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  48. setup_connection(options)
  49. keep_on_server = ActiveModel::Type::Boolean.new.cast(options[:keep_on_server])
  50. if options[:folder_id].present?
  51. folder_id = options[:folder_id]
  52. verify_folder!(folder_id, options)
  53. end
  54. # Taking first page of messages only effectivelly applies 1000-messages-in-one-go limit
  55. begin
  56. messages_details = @graph.list_messages(unread_only: keep_on_server, folder_id:, follow_pagination: false)
  57. message_ids = messages_details
  58. .fetch(:items)
  59. .pluck(:id)
  60. rescue MicrosoftGraph::ApiError => e
  61. Rails.logger.error "Unable to list emails from Microsoft Graph server (#{options[:user]}): #{e.inspect}"
  62. raise e
  63. end
  64. # fetch regular messages
  65. count_all = messages_details[:total_count]
  66. count = 0
  67. count_fetched = 0
  68. too_large_messages = []
  69. active_check_interval = 20
  70. result = 'ok'
  71. notice = ''
  72. message_ids.each do |message_id| # rubocop:disable Metrics/BlockLength
  73. count += 1
  74. break if (count % active_check_interval).zero? && channel_has_changed?(channel)
  75. Rails.logger.info " - message #{count}/#{count_all}"
  76. message_meta = @graph.get_message_basic_details(message_id)
  77. next if message_meta.nil?
  78. # ignore verify messages
  79. next if !messages_is_too_old_verify?(message_meta[:headers], count, count_all)
  80. # ignore already imported
  81. if already_imported?(message_meta[:headers], keep_on_server, channel)
  82. begin
  83. @graph.mark_message_as_read(message_id)
  84. Rails.logger.info "Ignore message #{count}/#{count_all}, because message message id already imported. Graph API Message ID: #{message_id}."
  85. rescue MicrosoftGraph::ApiError => e
  86. Rails.logger.error "Unable to mark email as read #{count}/#{count_all} from Microsoft Graph server (#{options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
  87. raise e
  88. end
  89. next
  90. end
  91. # delete email from server after article was created
  92. begin
  93. msg = @graph.get_raw_message(message_id)
  94. rescue MicrosoftGraph::ApiError => e
  95. Rails.logger.error "Unable to fetch email #{count}/#{count_all} from Microsoft Graph server (#{options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
  96. raise e
  97. end
  98. # do not process too big messages, instead download & send postmaster reply
  99. too_large_info = too_large?(message_meta[:size])
  100. if too_large_info
  101. if Setting.get('postmaster_send_reject_if_mail_too_large') == true
  102. info = " - download message #{count}/#{count_all} - ignore message because it's too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB) - Graph API Message ID: #{message_id}"
  103. Rails.logger.info info
  104. notice += "#{info}\n"
  105. process_oversized_mail(channel, msg)
  106. else
  107. info = " - ignore message #{count}/#{count_all} - because message is too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB) - Graph API Message ID: #{message_id}"
  108. Rails.logger.info info
  109. notice += "#{info}\n"
  110. too_large_messages.push info
  111. next
  112. end
  113. else
  114. process(channel, msg, false)
  115. end
  116. if keep_on_server
  117. begin
  118. @graph.mark_message_as_read(message_id)
  119. rescue MicrosoftGraph::ApiError => e
  120. Rails.logger.error "Unable to mark email as read #{count}/#{count_all} from Microsoft Graph server (#{options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
  121. raise e
  122. end
  123. else
  124. begin
  125. @graph.delete_message(message_id)
  126. rescue MicrosoftGraph::ApiError => e
  127. Rails.logger.error "Unable to delete #{count}/#{count_all} from Microsoft Graph server (#{options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
  128. raise e
  129. end
  130. end
  131. count_fetched += 1
  132. end
  133. if count.zero?
  134. Rails.logger.info ' - no message'
  135. end
  136. # Error is raised if one of the messages was too large AND postmaster_send_reject_if_mail_too_large is turned off.
  137. # This effectivelly marks channels as stuck and gets highlighted for the admin.
  138. # New emails are still processed! But large email is not touched, so error keeps being re-raised on every fetch.
  139. if too_large_messages.present?
  140. raise too_large_messages.join("\n")
  141. end
  142. {
  143. result: result,
  144. fetched: count_fetched,
  145. notice: notice,
  146. }
  147. end
  148. # Checks if mailbox has any messages.
  149. # It does not check if email is Zammad verification email or not like other drivers due to Graph API limitations.
  150. # X-Zammad-Verify and X-Zammad-Ignore headers are removed from mails sent via Graph API.
  151. # Thus it's not possible to verify Graph API connection by sending email with such header to yourself.
  152. def check_configuration(options)
  153. setup_connection(options)
  154. Rails.logger.info 'check only mode, fetch no emails'
  155. if options[:folder_id].present?
  156. folder_id = options[:folder_id]
  157. verify_folder!(folder_id, options)
  158. end
  159. begin
  160. messages_details = @graph.list_messages(folder_id:, follow_pagination: false)
  161. rescue MicrosoftGraph::ApiError => e
  162. Rails.logger.error "Unable to list emails from Microsoft Graph server (#{options[:user]}): #{e.inspect}"
  163. raise e
  164. end
  165. {
  166. result: 'ok',
  167. content_messages: messages_details[:total_count],
  168. }
  169. end
  170. def verify(_options, _verify_string)
  171. raise 'Microsoft Graph email channel is never verified. Thus this method is not implemented.' # rubocop:disable Zammad/DetectTranslatableString
  172. end
  173. private
  174. def setup_connection(options)
  175. access_token = options[:password]
  176. mailbox = options[:shared_mailbox].presence || options[:user]
  177. @graph = MicrosoftGraph.new access_token:, mailbox:
  178. end
  179. def verify_folder!(id, options)
  180. @graph.get_message_folder_details(id)
  181. rescue MicrosoftGraph::ApiError => e
  182. raise e if e.error_code != 'ErrorInvalidIdMalformed'
  183. Rails.logger.error "Unable to fetch email from folder at Microsoft Graph/#{options[:user]} Folder does not exist: #{id}"
  184. raise "Microsoft Graph email folder does not exist: #{id}"
  185. end
  186. end