-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: Support for Microsoft Oauth in Email Channel #6227
Changes from 34 commits
f3c4fcf
cab4aa3
579bebc
bb5a248
3247067
d91d1dc
44a55ac
c71c602
7a58a70
330a36e
96368e8
5f99ffa
48bf5b5
b10ccb6
90471d7
7f43ab5
cb701d6
3fe0d06
cd9e93d
d8acd5f
3c23975
b10d0f3
8deac0f
de416ad
85abe97
7cc3113
7521813
d55ff39
179fb37
8c668f6
d5a8e05
f0bcdda
4b28672
d53b077
2708fc2
5d712dd
526b83a
9d941de
799f46b
5eab1de
f18d7a5
8e55349
908c409
6a9f78c
6adebd1
80d977b
7da09f6
cae923b
ba09939
10cf72c
759fecf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController | ||
include MicrosoftConcern | ||
before_action :check_authorization | ||
|
||
def create | ||
email = params[:authorization][:email] | ||
redirect_url = microsoft_client.auth_code.authorize_url({ | ||
redirect_uri: "#{base_url}/microsoft/callback", | ||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All | ||
https://outlook.office.com/SMTP.Send openid', | ||
prompt: 'consent' | ||
}) | ||
if redirect_url | ||
::Redis::Alfred.setex(email, Current.account.id, 5.minutes) | ||
render json: { success: true, url: redirect_url } | ||
else | ||
render json: { success: false }, status: :unprocessable_entity | ||
end | ||
end | ||
|
||
private | ||
|
||
def check_authorization | ||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator? | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
module MicrosoftConcern | ||
extend ActiveSupport::Concern | ||
|
||
def microsoft_client | ||
::OAuth2::Client.new(ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil), | ||
{ | ||
site: 'https://login.microsoftonline.com', | ||
authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', | ||
token_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token' | ||
}) | ||
end | ||
|
||
private | ||
|
||
def parsed_body | ||
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body) | ||
end | ||
|
||
def base_url | ||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000') | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
class Microsoft::CallbacksController < ApplicationController | ||
include MicrosoftConcern | ||
|
||
def show | ||
@response = microsoft_client.auth_code.get_token( | ||
oauth_code, | ||
redirect_uri: "#{base_url}/microsoft/callback" | ||
) | ||
|
||
ActiveRecord::Base.transaction do | ||
inbox = find_or_create_inbox | ||
::Redis::Alfred.delete(users_data['email']) | ||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id) | ||
rescue StandardError => e | ||
Rails.logger.error e | ||
redirect_to microsoft_app_redirect_url | ||
end | ||
end | ||
|
||
private | ||
|
||
def oauth_code | ||
params[:code] | ||
end | ||
|
||
def users_data | ||
decoded_token = JWT.decode parsed_body[:id_token], nil, false | ||
decoded_token[0] | ||
end | ||
|
||
def parsed_body | ||
@parsed_body ||= @response.response.parsed | ||
end | ||
|
||
def account_id | ||
::Redis::Alfred.get(users_data['email']) | ||
end | ||
|
||
def account | ||
@account ||= Account.find(account_id) | ||
end | ||
|
||
def microsoft_app_redirect_url | ||
app_new_microsoft_inbox_url(account_id: account.id) | ||
end | ||
|
||
def find_or_create_inbox | ||
channel_email = create_imap_email_channel | ||
|
||
return channel_email.inbox if channel_email.inbox.presence | ||
|
||
account.inboxes.create_or_find_by!( | ||
account: account, | ||
channel: channel_email, | ||
name: users_data['name'] | ||
) | ||
end | ||
|
||
def create_imap_email_channel | ||
channel_email = Channel::Email.find_or_create_by!(email: users_data['email'], account: account) | ||
channel_email.update!({ | ||
imap_login: users_data['email'], imap_address: 'outlook.office365.com', | ||
imap_port: '993', imap_enabled: true, | ||
provider: 'microsoft', | ||
provider_config: { | ||
access_token: parsed_body['access_token'], | ||
refresh_token: parsed_body['refresh_token'], | ||
expires_on: (Time.current.utc + 1.hour).to_s | ||
} | ||
}) | ||
channel_email | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,10 @@ | |
"ENABLE": "Create conversations from mentioned Tweets" | ||
} | ||
}, | ||
"MICROSOFT": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be a part of the frontend PR ? |
||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ", | ||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again" | ||
}, | ||
"WEBSITE_CHANNEL": { | ||
"TITLE": "Website channel", | ||
"DESC": "Create a channel for your website and start supporting your customers via our website widget.", | ||
|
@@ -548,6 +552,10 @@ | |
}, | ||
"ENABLE_SSL": "Enable SSL" | ||
}, | ||
"MICROSOFT": { | ||
"TITLE": "Microsoft", | ||
"SUBTITLE": "Reauthorize your MICROSOFT account" | ||
}, | ||
"SMTP": { | ||
"TITLE": "SMTP", | ||
"SUBTITLE": "Set your SMTP details", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,10 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob | |
def perform(channel) | ||
return unless should_fetch_email?(channel) | ||
|
||
# fetching email for microsoft provider | ||
fetch_mail_for_ms_provider(channel) | ||
fetch_mail_for_channel(channel) | ||
|
||
# clearing old failures like timeouts since the mail is now successfully processed | ||
channel.reauthorized! | ||
rescue *ExceptionList::IMAP_EXCEPTIONS | ||
|
@@ -24,6 +27,8 @@ def should_fetch_email?(channel) | |
end | ||
|
||
def fetch_mail_for_channel(channel) | ||
return if channel.microsoft? | ||
|
||
# TODO: rather than setting this as default method for all mail objects, lets if can do new mail object | ||
# using Mail.retriever_method.new(params) | ||
Mail.defaults do | ||
|
@@ -41,9 +46,51 @@ def fetch_mail_for_channel(channel) | |
end | ||
end | ||
|
||
def fetch_mail_for_ms_provider(channel) | ||
return unless channel.microsoft? && channel.provider_config[:access_token].blank? | ||
|
||
access_token = valid_access_token channel | ||
|
||
return unless access_token | ||
|
||
imap = imap_authenticate(channel, access_token) | ||
|
||
process_mails(imap, channel) | ||
end | ||
|
||
def process_mails(imap, channel) | ||
imap.search(['BEFORE', tomorrow, 'SINCE', yesterday]).each do |message_id| | ||
inbound_mail = Mail.read_from_string imap.fetch(message_id, 'RFC822')[0].attr['RFC822'] | ||
|
||
next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present? | ||
|
||
process_mail(inbound_mail, channel) | ||
end | ||
end | ||
|
||
def imap_authenticate(channel, access_token) | ||
imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true) | ||
imap.authenticate('XOAUTH2', channel.imap_login, access_token) | ||
imap.select('INBOX') | ||
imap | ||
end | ||
|
||
def yesterday | ||
(Time.zone.today - 1).strftime('%d-%b-%Y') | ||
end | ||
|
||
def tomorrow | ||
(Time.zone.today + 1).strftime('%d-%b-%Y') | ||
end | ||
|
||
def process_mail(inbound_mail, channel) | ||
Imap::ImapMailbox.new.process(inbound_mail, channel) | ||
rescue StandardError => e | ||
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception | ||
end | ||
|
||
# Making sure the access token is valid for microsoft provider | ||
def valid_access_token(channel) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will be doing this step every time when we fetch the mail right ? ( in 1 minute ). In case we won't be needing to run the job separately in a schedule ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yes! you are right :D.. This is happening every 5 minute to check if the token is still valid and if not it will request for new token. Removing the refresh token job. |
||
Microsoft::RefreshOauthTokenService.new(channel: channel).access_token | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tejaswinichile net::imap latest versions support xoauth automatically. We can upgrade the package. No need to do it in this PR. We can take it as a separate task.