# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ class ExternalCredential::Google def self.app_verify(params) request_account_to_link(params, false) params end def self.request_account_to_link(credentials = {}, app_required = true) external_credential = ExternalCredential.find_by(name: 'google') raise Exceptions::UnprocessableEntity, __('There is no Google app configured.') if !external_credential && app_required if external_credential if credentials[:client_id].blank? credentials[:client_id] = external_credential.credentials['client_id'] end if credentials[:client_secret].blank? credentials[:client_secret] = external_credential.credentials['client_secret'] end end raise Exceptions::UnprocessableEntity, __("The required parameter 'client_id' is missing.") if credentials[:client_id].blank? raise Exceptions::UnprocessableEntity, __("The required parameter 'client_secret' is missing.") if credentials[:client_secret].blank? authorize_url = generate_authorize_url(credentials[:client_id]) { authorize_url: authorize_url, } end def self.link_account(_request_token, params) external_credential = ExternalCredential.find_by(name: 'google') raise Exceptions::UnprocessableEntity, __('There is no Google app configured.') if !external_credential raise Exceptions::UnprocessableEntity, __("The required parameter 'code' is missing.") if !params[:code] response = authorize_tokens(external_credential.credentials[:client_id], external_credential.credentials[:client_secret], params[:code]) %w[refresh_token access_token expires_in scope token_type id_token].each do |key| raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank? end user_data = user_info(response[:id_token]) raise Exceptions::UnprocessableEntity, __("User email could not be extracted from 'id_token'.") if user_data[:email].blank? channel_options = { inbound: { adapter: 'imap', options: { auth_type: 'XOAUTH2', host: 'imap.gmail.com', ssl: 'ssl', ssl_verify: true, user: user_data[:email], }, }, outbound: { adapter: 'smtp', options: { host: 'smtp.gmail.com', port: 465, ssl: true, ssl_verify: true, user: user_data[:email], authentication: 'xoauth2', }, }, auth: response.merge( provider: 'google', type: 'XOAUTH2', client_id: external_credential.credentials[:client_id], client_secret: external_credential.credentials[:client_secret], ), } if params[:channel_id] existing_channel = Channel.where(area: 'Google::Account').find(params[:channel_id]) channel_options[:inbound][:options][:folder] = existing_channel.options[:inbound][:options][:folder] channel_options[:inbound][:options][:keep_on_server] = existing_channel.options[:inbound][:options][:keep_on_server] existing_channel.update!( options: channel_options, ) existing_channel.refresh_xoauth2! return existing_channel end migrate_channel = nil Channel.where(area: 'Email::Account').find_each do |channel| next if channel.options.dig(:inbound, :options, :host)&.downcase != 'imap.gmail.com' next if channel.options.dig(:outbound, :options, :host)&.downcase != 'smtp.gmail.com' next if channel.options.dig(:outbound, :options, :user)&.downcase != user_data[:email].downcase && channel.options.dig(:outbound, :email)&.downcase != user_data[:email].downcase migrate_channel = channel break end if migrate_channel channel_options[:inbound][:options][:folder] = migrate_channel.options[:inbound][:options][:folder] channel_options[:inbound][:options][:keep_on_server] = migrate_channel.options[:inbound][:options][:keep_on_server] backup = { attributes: { area: migrate_channel.area, options: migrate_channel.options, last_log_in: migrate_channel.last_log_in, last_log_out: migrate_channel.last_log_out, status_in: migrate_channel.status_in, status_out: migrate_channel.status_out, }, migrated_at: Time.zone.now, } migrate_channel.update( area: 'Google::Account', options: channel_options.merge(backup_imap_classic: backup), last_log_in: nil, last_log_out: nil, ) return migrate_channel end email_addresses = user_aliases(response) email_addresses.unshift({ name: "#{Setting.get('product_name')} Support", email: user_data[:email], }) email_addresses.each do |email| next if !EmailAddress.exists?(email: email[:email]) raise Exceptions::UnprocessableEntity, "Duplicate email address or email alias #{email[:email]} found!" end # create channel channel = Channel.create!( area: 'Google::Account', group_id: Group.first.id, options: channel_options, active: false, created_by_id: 1, updated_by_id: 1, ) email_addresses.each do |user_alias| EmailAddress.create!( channel_id: channel.id, name: user_alias[:name], email: user_alias[:email], active: true, created_by_id: 1, updated_by_id: 1, ) end channel end def self.generate_authorize_url(client_id, scope = 'openid email profile https://mail.google.com/') params = { 'client_id' => client_id, 'redirect_uri' => ExternalCredential.callback_url('google'), 'scope' => scope, 'response_type' => 'code', 'access_type' => 'offline', 'prompt' => 'consent', } uri = URI::HTTPS.build( host: 'accounts.google.com', path: '/o/oauth2/auth', query: params.to_query ) uri.to_s end def self.authorize_tokens(client_id, client_secret, authorization_code) params = { 'client_secret' => client_secret, 'code' => authorization_code, 'grant_type' => 'authorization_code', 'client_id' => client_id, 'redirect_uri' => ExternalCredential.callback_url('google'), } uri = URI::HTTPS.build( host: 'accounts.google.com', path: '/o/oauth2/token', ) response = UserAgent.post(uri.to_s, params) if response.code != 200 && response.body.blank? Rails.logger.error "Request failed! (code: #{response.code})" raise "Request failed! (code: #{response.code})" end result = JSON.parse(response.body) if result['error'] && response.code != 200 Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})" raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})" end result[:created_at] = Time.zone.now result.symbolize_keys end def self.refresh_token(token) return token if token[:created_at] >= 50.minutes.ago params = { 'client_id' => token[:client_id], 'client_secret' => token[:client_secret], 'refresh_token' => token[:refresh_token], 'grant_type' => 'refresh_token', } uri = URI::HTTPS.build( host: 'accounts.google.com', path: '/o/oauth2/token', ) response = UserAgent.post(uri.to_s, params) if response.code != 200 && response.body.blank? Rails.logger.error "Request failed! (code: #{response.code})" raise "Request failed! (code: #{response.code})" end result = JSON.parse(response.body) if result['error'] && response.code != 200 Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})" raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})" end token.merge( created_at: Time.zone.now, access_token: result['access_token'], ).symbolize_keys end def self.user_aliases(token) uri = URI.parse('https://www.googleapis.com/gmail/v1/users/me/settings/sendAs') http = UserAgent.get_http(uri, {}) http.use_ssl = true response = http.get(uri.request_uri, { 'Authorization' => "#{token[:token_type]} #{token[:access_token]}" }) if response.code != 200 && response.body.blank? Rails.logger.error "Request failed! (code: #{response.code})" raise "Request failed! (code: #{response.code})" end result = JSON.parse(response.body) if result['error'] && response.code != 200 Rails.logger.error "Request failed! ERROR: #{result['error']['message']}" raise "Request failed! ERROR: #{result['error']['message']}" end aliases = [] result['sendAs'].each do |row| next if row['isPrimary'] next if !row['verificationStatus'] next if row['verificationStatus'] != 'accepted' aliases.push({ name: row['displayName'], email: row['sendAsEmail'], }) end aliases end def self.user_info(id_token) split = id_token.split('.')[1] return if split.blank? JSON.parse(Base64.decode64(split)).symbolize_keys end end