google.rb 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class ExternalCredential::Google < ExternalCredential::Base::ChannelXoauth2
  3. def self.channel_area
  4. 'Google::Account'.freeze
  5. end
  6. def self.app_verify(params)
  7. request_account_to_link(params, false)
  8. params
  9. end
  10. def self.request_account_to_link(credentials = {}, app_required = true)
  11. external_credential = ExternalCredential.find_by(name: 'google')
  12. raise Exceptions::UnprocessableEntity, __('There is no Google app configured.') if !external_credential && app_required
  13. if external_credential
  14. if credentials[:client_id].blank?
  15. credentials[:client_id] = external_credential.credentials['client_id']
  16. end
  17. if credentials[:client_secret].blank?
  18. credentials[:client_secret] = external_credential.credentials['client_secret']
  19. end
  20. end
  21. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_id' is missing.") if credentials[:client_id].blank?
  22. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_secret' is missing.") if credentials[:client_secret].blank?
  23. authorize_url = generate_authorize_url(credentials[:client_id])
  24. {
  25. authorize_url: authorize_url,
  26. }
  27. end
  28. def self.link_account(_request_token, params)
  29. external_credential = ExternalCredential.find_by(name: 'google')
  30. raise Exceptions::UnprocessableEntity, __('There is no Google app configured.') if !external_credential
  31. raise Exceptions::UnprocessableEntity, __("The required parameter 'code' is missing.") if !params[:code]
  32. response = authorize_tokens(external_credential.credentials[:client_id], external_credential.credentials[:client_secret], params[:code])
  33. %w[refresh_token access_token expires_in scope token_type id_token].each do |key|
  34. raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank?
  35. end
  36. user_data = user_info(response[:id_token])
  37. raise Exceptions::UnprocessableEntity, __("User email could not be extracted from 'id_token'.") if user_data[:email].blank?
  38. channel_options = {
  39. inbound: {
  40. adapter: 'imap',
  41. options: {
  42. auth_type: 'XOAUTH2',
  43. host: 'imap.gmail.com',
  44. ssl: 'ssl',
  45. ssl_verify: true,
  46. user: user_data[:email],
  47. },
  48. },
  49. outbound: {
  50. adapter: 'smtp',
  51. options: {
  52. host: 'smtp.gmail.com',
  53. port: 465,
  54. ssl: true,
  55. ssl_verify: true,
  56. user: user_data[:email],
  57. authentication: 'xoauth2',
  58. },
  59. },
  60. auth: response.merge(
  61. provider: 'google',
  62. type: 'XOAUTH2',
  63. client_id: external_credential.credentials[:client_id],
  64. client_secret: external_credential.credentials[:client_secret],
  65. ),
  66. }
  67. if params[:channel_id]
  68. existing_channel = Channel.where(area: 'Google::Account').find(params[:channel_id])
  69. channel_options[:inbound][:options][:folder] = existing_channel.options[:inbound][:options][:folder]
  70. channel_options[:inbound][:options][:keep_on_server] = existing_channel.options[:inbound][:options][:keep_on_server]
  71. existing_channel.update!(
  72. options: channel_options,
  73. )
  74. existing_channel.refresh_xoauth2!
  75. return existing_channel
  76. end
  77. migrate_channel = nil
  78. Channel.where(area: 'Email::Account').find_each do |channel|
  79. next if channel.options.dig(:inbound, :options, :host)&.downcase != 'imap.gmail.com'
  80. next if channel.options.dig(:outbound, :options, :host)&.downcase != 'smtp.gmail.com'
  81. next if channel.options.dig(:outbound, :options, :user)&.downcase != user_data[:email].downcase && channel.options.dig(:outbound, :email)&.downcase != user_data[:email].downcase
  82. migrate_channel = channel
  83. break
  84. end
  85. if migrate_channel
  86. channel_options[:inbound][:options][:folder] = migrate_channel.options[:inbound][:options][:folder]
  87. channel_options[:inbound][:options][:keep_on_server] = migrate_channel.options[:inbound][:options][:keep_on_server]
  88. backup = {
  89. attributes: {
  90. area: migrate_channel.area,
  91. options: migrate_channel.options,
  92. last_log_in: migrate_channel.last_log_in,
  93. last_log_out: migrate_channel.last_log_out,
  94. status_in: migrate_channel.status_in,
  95. status_out: migrate_channel.status_out,
  96. },
  97. migrated_at: Time.zone.now,
  98. }
  99. migrate_channel.update(
  100. area: 'Google::Account',
  101. options: channel_options.merge(backup_imap_classic: backup),
  102. last_log_in: nil,
  103. last_log_out: nil,
  104. )
  105. return migrate_channel
  106. end
  107. email_addresses = user_aliases(response)
  108. email_addresses.unshift({
  109. name: "#{Setting.get('product_name')} Support",
  110. email: user_data[:email],
  111. })
  112. email_addresses.each do |email|
  113. next if !EmailAddress.exists?(email: email[:email])
  114. raise Exceptions::UnprocessableEntity, "Duplicate email address or email alias #{email[:email]} found!"
  115. end
  116. # create channel
  117. channel = Channel.create!(
  118. area: 'Google::Account',
  119. group_id: Group.first.id,
  120. options: channel_options,
  121. active: false,
  122. created_by_id: 1,
  123. updated_by_id: 1,
  124. )
  125. email_addresses.each do |user_alias|
  126. EmailAddress.create!(
  127. channel_id: channel.id,
  128. name: user_alias[:name],
  129. email: user_alias[:email],
  130. active: true,
  131. created_by_id: 1,
  132. updated_by_id: 1,
  133. )
  134. end
  135. channel
  136. end
  137. def self.generate_authorize_url(client_id, scope = 'openid email profile https://mail.google.com/')
  138. params = {
  139. 'client_id' => client_id,
  140. 'redirect_uri' => ExternalCredential.callback_url('google'),
  141. 'scope' => scope,
  142. 'response_type' => 'code',
  143. 'access_type' => 'offline',
  144. 'prompt' => 'consent',
  145. }
  146. uri = URI::HTTPS.build(
  147. host: 'accounts.google.com',
  148. path: '/o/oauth2/auth',
  149. query: params.to_query
  150. )
  151. uri.to_s
  152. end
  153. def self.authorize_tokens(client_id, client_secret, authorization_code)
  154. params = {
  155. 'client_secret' => client_secret,
  156. 'code' => authorization_code,
  157. 'grant_type' => 'authorization_code',
  158. 'client_id' => client_id,
  159. 'redirect_uri' => ExternalCredential.callback_url('google'),
  160. }
  161. uri = URI::HTTPS.build(
  162. host: 'accounts.google.com',
  163. path: '/o/oauth2/token',
  164. )
  165. response = UserAgent.post(uri.to_s, params)
  166. if response.code != 200 && response.body.blank?
  167. Rails.logger.error "Request failed! (code: #{response.code})"
  168. raise "Request failed! (code: #{response.code})"
  169. end
  170. result = JSON.parse(response.body)
  171. if result['error'] && response.code != 200
  172. Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  173. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  174. end
  175. result[:created_at] = Time.zone.now
  176. result.symbolize_keys
  177. end
  178. def self.refresh_token(token)
  179. return token if token[:created_at] >= 50.minutes.ago
  180. params = {
  181. 'client_id' => token[:client_id],
  182. 'client_secret' => token[:client_secret],
  183. 'refresh_token' => token[:refresh_token],
  184. 'grant_type' => 'refresh_token',
  185. }
  186. uri = URI::HTTPS.build(
  187. host: 'accounts.google.com',
  188. path: '/o/oauth2/token',
  189. )
  190. response = UserAgent.post(uri.to_s, params)
  191. if response.code != 200 && response.body.blank?
  192. Rails.logger.error "Request failed! (code: #{response.code})"
  193. raise "Request failed! (code: #{response.code})"
  194. end
  195. result = JSON.parse(response.body)
  196. if result['error'] && response.code != 200
  197. Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  198. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  199. end
  200. token.merge(
  201. created_at: Time.zone.now,
  202. access_token: result['access_token'],
  203. ).symbolize_keys
  204. end
  205. def self.user_aliases(token)
  206. uri = URI.parse('https://www.googleapis.com/gmail/v1/users/me/settings/sendAs')
  207. http = UserAgent.get_http(uri, {})
  208. http.use_ssl = true
  209. response = http.get(uri.request_uri, { 'Authorization' => "#{token[:token_type]} #{token[:access_token]}" })
  210. if response.code != 200 && response.body.blank?
  211. Rails.logger.error "Request failed! (code: #{response.code})"
  212. raise "Request failed! (code: #{response.code})"
  213. end
  214. result = JSON.parse(response.body)
  215. if result['error'] && response.code != 200
  216. Rails.logger.error "Request failed! ERROR: #{result['error']['message']}"
  217. raise "Request failed! ERROR: #{result['error']['message']}"
  218. end
  219. aliases = []
  220. result['sendAs'].each do |row|
  221. next if row['isPrimary']
  222. next if !row['verificationStatus']
  223. next if row['verificationStatus'] != 'accepted'
  224. aliases.push({
  225. name: row['displayName'],
  226. email: row['sendAsEmail'],
  227. })
  228. end
  229. aliases
  230. end
  231. def self.user_info(id_token)
  232. split = id_token.split('.')[1]
  233. return if split.blank?
  234. JSON.parse(Base64.decode64(split)).symbolize_keys
  235. end
  236. end