123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
- class Channel::Driver::MicrosoftGraphInbound < Channel::Driver::BaseEmailInbound
- # Fetches emails from Microsfot 365 account via Graph API
- #
- # @param options [Hash]
- # @option options [Bool, String] :keep_on_server
- # @option options [String] :folder_id to fetch emails from
- # @option options [String] :user to login with
- # @option options [String] :shared_mailbox optional
- # @option options [String] :password Graph API access token
- # @option options [String] :auth_type must be XOAUTH2
- # @param channel [Channel]
- #
- # @return [Hash]
- #
- # {
- # result: 'ok',
- # fetched: 123,
- # notice: 'e. g. message about to big emails in mailbox',
- # }
- #
- # @example
- #
- # params = {
- # user: 'xxx@zammad.onmicrosoft.com',
- # password: 'xxx',
- # shared_mailbox: 'yyy@zammad.onmicrosoft.com',
- # keep_on_server: true,
- # auth_type: 'XOAUTH2'
- # }
- #
- # channel = Channel.last
- # instance = Channel::Driver::MicrosoftGraphInbound.new
- # result = instance.fetch(params, channel)
- def fetch(...) # rubocop:disable Lint/UselessMethodDefinition
- # fetch() method is defined in superclass, but options are subclass-specific,
- # so define it here for documentation purposes.
- super
- end
- # Checks if mailbox has any messages.
- # It does not check if email is Zammad verification email or not like other drivers due to Graph API limitations.
- # X-Zammad-Verify and X-Zammad-Ignore headers are removed from mails sent via Graph API.
- # Thus it's not possible to verify Graph API connection by sending email with such header to yourself.
- def check_configuration(options)
- setup_connection(options)
- _collection, count_all = messages_iterator(false, options)
- Rails.logger.info 'check only mode, fetch no emails'
- {
- result: 'ok',
- content_messages: count_all,
- }
- end
- def verify_transport(_options, _verify_string)
- raise 'Microsoft Graph email channel is never verified. Thus this method is not implemented.' # rubocop:disable Zammad/DetectTranslatableString
- end
- def fetch_single_message(message_id, count, count_all) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
- message_meta = @graph.get_message_basic_details(message_id)
- message_validator = MessageValidator.new(message_meta[:headers], message_meta[:size])
- # ignore fresh verify messages
- if message_validator.fresh_verify_message?
- Rails.logger.info " - ignore message #{count}/#{count_all} - because message has a verify message"
- return MessageResult.new(success: false)
- end
- # ignore already imported
- if message_validator.already_imported?(@keep_on_server, @channel)
- begin
- @graph.mark_message_as_read(message_id)
- Rails.logger.info "Ignore message #{count}/#{count_all}, because message message id already imported. Graph API Message ID: #{message_id}."
- rescue MicrosoftGraph::ApiError => e
- 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}"
- raise e
- end
- return MessageResult.new(success: false)
- end
- # delete email from server after article was created
- begin
- msg = @graph.get_raw_message(message_id)
- rescue MicrosoftGraph::ApiError => e
- Rails.logger.error "Unable to fetch email #{count}/#{count_all} from Microsoft Graph server (#{@options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
- raise e
- end
- # do not process too big messages, instead download & send postmaster reply
- too_large_info = message_validator.too_large?
- if too_large_info
- if Setting.get('postmaster_send_reject_if_mail_too_large') == true
- 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}"
- Rails.logger.info info
- after_action = [:notice, "#{info}\n"]
- process_oversized_mail(@channel, msg)
- else
- 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}"
- Rails.logger.info info
- return MessageResult.new(success: false, after_action: [:too_large_ignored, "#{info}\n"])
- end
- else
- process(@channel, msg, false)
- end
- if @keep_on_server
- begin
- @graph.mark_message_as_read(message_id)
- rescue MicrosoftGraph::ApiError => e
- 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}"
- raise e
- end
- else
- begin
- @graph.delete_message(message_id)
- rescue MicrosoftGraph::ApiError => e
- Rails.logger.error "Unable to delete #{count}/#{count_all} from Microsoft Graph server (#{@options[:user]}). Graph API Message ID: #{message_id}. #{e.inspect}"
- raise e
- end
- end
- MessageResult.new(success: true, after_action: after_action)
- end
- def messages_iterator(keep_on_server, options)
- if options[:folder_id].present?
- folder_id = options[:folder_id]
- verify_folder!(folder_id, options)
- end
- # Taking first page of messages only effectivelly applies 1000-messages-in-one-go limit
- messages_details = @graph.list_messages(unread_only: keep_on_server, folder_id:, follow_pagination: false)
- ids = messages_details.fetch(:items).pluck(:id)
- count = messages_details.fetch(:total_count)
- [ids, count]
- rescue MicrosoftGraph::ApiError => e
- Rails.logger.error "Unable to list emails from Microsoft Graph server (#{options[:user]}): #{e.inspect}"
- raise e
- end
- private
- def setup_connection(options)
- access_token = options[:password]
- mailbox = options[:shared_mailbox].presence || options[:user]
- @graph = MicrosoftGraph.new access_token:, mailbox:
- end
- def verify_folder!(id, options)
- @graph.get_message_folder_details(id)
- rescue MicrosoftGraph::ApiError => e
- raise e if e.error_code != 'ErrorInvalidIdMalformed'
- Rails.logger.error "Unable to fetch email from folder at Microsoft Graph/#{options[:user]} Folder does not exist: #{id}"
- raise "Microsoft Graph email folder does not exist: #{id}"
- end
- end
|