google.rb 8.7 KB

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