feat(microsoft-shared): Phase 1 — OAuth + send via Graph API
Adds a new "microsoft_shared" Email Channel provider that uses Microsoft Graph API end-to-end for sending. Bypasses xoauth2 SMTP, so it works on tenants with Security Defaults enabled and survives the April 2026 SMTP Basic Auth retirement. Flow: 1. Admin picks "Microsoft Shared Inbox" in the Add Inbox wizard 2. Enters the shared mailbox UPN (e.g. sales@company.com) 3. Single OAuth round-trip as a delegate user; UPN is carried in a signed state parameter so the callback knows which mailbox to bind 4. Callback verifies the OAuth user has access to the UPN via GET /users/{upn}/mailFolders/inbox before creating the channel 5. Replies route through a custom :microsoft_shared ActionMailer delivery method that POSTs base64-encoded MIME to /users/{upn}/sendMail Channel modifications use the custom/ overlay (Custom::Leadchat::*). Direct OSS edits are wrapped in '# === LeadChat: microsoft_shared ===' markers. Phase 2 (Graph webhook receive) and Phase 3 (re-auth, subscription failure surfacing, channel-destroy cleanup) follow. Azure app prerequisites (one-time, manual): add delegated permissions Mail.Send.Shared, Mail.ReadWrite.Shared, User.Read, offline_access; grant admin consent. No new app registration required. See LEADCHAT.md for the full customization index.
This commit is contained in:
13
LEADCHAT.md
13
LEADCHAT.md
@@ -17,6 +17,7 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
| Hook in OSS file | Overlay file | Adds |
|
||||
|---|---|---|
|
||||
| `app/models/channel/email.rb` (bottom) | `custom/app/models/custom/leadchat/channel/email_extension.rb` | `Channel::Email#microsoft_shared?` predicate |
|
||||
| `app/mailers/conversation_reply_mailer_helper.rb` (bottom) | `custom/app/mailers/custom/leadchat/conversation_reply_mailer_helper_extension.rb` | Routes `microsoft_shared` channels to the `:microsoft_shared` ActionMailer delivery method (Graph API), bypassing xoauth2 SMTP |
|
||||
|
||||
### B. Direct edits to OSS files
|
||||
|
||||
@@ -24,6 +25,10 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
|---|---|---|
|
||||
| `config/application.rb` | Mirror enterprise/ autoload setup for `custom/` so `Custom::*` modules are autoloaded and `custom/config/initializers/**/*.rb` are required | `# === LeadChat: custom/ overlay autoload ===` |
|
||||
| `app/models/channel/email.rb` (bottom) | One-line `include_mod_with` hook for the email_extension overlay | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/mailers/conversation_reply_mailer_helper.rb` (bottom) | One-line `prepend_mod_with` hook for the helper extension overlay | `# === LeadChat: microsoft_shared ===` |
|
||||
| `config/routes.rb` (3 separate blocks) | (1) `app_new_microsoft_shared_inbox` dashboard route; (2) account-namespaced `microsoft_shared/authorization` API endpoint; (3) `microsoft_shared/callback` OAuth callback | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue` (3 separate blocks) | (1) import `MicrosoftShared.vue`; (2) `emailProviderList` entry for `microsoft_shared`; (3) router slot `<MicrosoftShared v-else-if="provider === 'microsoft_shared'"/>` | `<!-- === LeadChat: microsoft_shared === -->` and `// === LeadChat: microsoft_shared ===` |
|
||||
| `app/javascript/dashboard/i18n/locale/en/inboxMgmt.json` | New keys: `INBOX_MGMT.ADD.MICROSOFT_SHARED.*` and `INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT_SHARED.*` | (no comment markers — JSON; flagged here in LEADCHAT.md instead) |
|
||||
|
||||
### C. New top-level files (no upstream conflict potential)
|
||||
|
||||
@@ -31,6 +36,14 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
|---|---|
|
||||
| `LEADCHAT.md` | This file |
|
||||
| `custom/` | Root of LeadChat overlay tree, picked up by `ChatwootApp.custom?` and the Phase-0 autoload setup |
|
||||
| `custom/config/initializers/microsoft_shared_delivery.rb` | Registers `:microsoft_shared` ActionMailer delivery method |
|
||||
| `app/controllers/concerns/microsoft_shared_concern.rb` | OAuth client + Graph scopes + signed-state encoding/decoding for the new provider |
|
||||
| `app/controllers/api/v1/accounts/microsoft_shared/authorizations_controller.rb` | Accepts shared mailbox UPN, returns Microsoft authorize URL with UPN embedded in signed state |
|
||||
| `app/controllers/microsoft_shared/callbacks_controller.rb` | OAuth callback: decodes state, exchanges code, verifies UPN access via Graph, creates `Channel::Email` + `Inbox` |
|
||||
| `app/services/microsoft/shared/send_mail_service.rb` | Builds RFC 822 MIME and POSTs base64-encoded to `/users/{upn}/sendMail` via Graph |
|
||||
| `app/mailers/microsoft_shared_delivery.rb` | Custom ActionMailer delivery method that delegates to the send service |
|
||||
| `app/javascript/dashboard/api/channel/microsoftSharedClient.js` | Frontend API client |
|
||||
| `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/MicrosoftShared.vue` | Single-step UPN entry form before the OAuth redirect |
|
||||
|
||||
### D. Brand script targets
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
class Api::V1::Accounts::MicrosoftShared::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||
include MicrosoftSharedConcern
|
||||
|
||||
def create
|
||||
upn = params[:shared_mailbox_upn].to_s.strip.downcase
|
||||
return render(json: { success: false, error: 'shared_mailbox_upn is required' }, status: :unprocessable_entity) if upn.blank?
|
||||
|
||||
redirect_url = microsoft_shared_client.auth_code.authorize_url(
|
||||
redirect_uri: "#{base_url}/microsoft_shared/callback",
|
||||
scope: microsoft_shared_scope,
|
||||
state: encode_microsoft_shared_state(Current.account, upn)
|
||||
)
|
||||
|
||||
if redirect_url
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/controllers/concerns/microsoft_shared_concern.rb
Normal file
48
app/controllers/concerns/microsoft_shared_concern.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module MicrosoftSharedConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
STATE_VERIFIER_PURPOSE = :microsoft_shared_oauth_state
|
||||
STATE_TTL = 15.minutes
|
||||
|
||||
GRAPH_SCOPES = %w[
|
||||
offline_access
|
||||
openid
|
||||
profile
|
||||
email
|
||||
User.Read
|
||||
Mail.Send.Shared
|
||||
Mail.ReadWrite.Shared
|
||||
].freeze
|
||||
|
||||
def microsoft_shared_client
|
||||
app_id = GlobalConfigService.load('AZURE_APP_ID', nil)
|
||||
app_secret = GlobalConfigService.load('AZURE_APP_SECRET', nil)
|
||||
|
||||
::OAuth2::Client.new(
|
||||
app_id,
|
||||
app_secret,
|
||||
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 microsoft_shared_scope
|
||||
GRAPH_SCOPES.join(' ')
|
||||
end
|
||||
|
||||
def encode_microsoft_shared_state(account, shared_mailbox_upn)
|
||||
Rails.application.message_verifier(STATE_VERIFIER_PURPOSE).generate(
|
||||
{ account_gid: account.to_global_id.to_s, upn: shared_mailbox_upn },
|
||||
expires_in: STATE_TTL
|
||||
)
|
||||
end
|
||||
|
||||
def decode_microsoft_shared_state(raw_state)
|
||||
Rails.application.message_verifier(STATE_VERIFIER_PURPOSE).verify(raw_state)
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
|
||||
nil
|
||||
end
|
||||
end
|
||||
106
app/controllers/microsoft_shared/callbacks_controller.rb
Normal file
106
app/controllers/microsoft_shared/callbacks_controller.rb
Normal file
@@ -0,0 +1,106 @@
|
||||
class MicrosoftShared::CallbacksController < ApplicationController
|
||||
include MicrosoftSharedConcern
|
||||
|
||||
GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0'.freeze
|
||||
|
||||
def show
|
||||
state_payload = decode_microsoft_shared_state(params[:state])
|
||||
return redirect_with_error('Invalid or expired state.') if state_payload.blank?
|
||||
|
||||
@account = GlobalID::Locator.locate(state_payload[:account_gid])
|
||||
return redirect_with_error('Account not found.') if @account.blank?
|
||||
|
||||
@upn = state_payload[:upn].to_s.strip.downcase
|
||||
return redirect_with_error('Shared mailbox address missing.') if @upn.blank?
|
||||
|
||||
@response = exchange_code_for_tokens
|
||||
return redirect_with_error('Token exchange failed.') if @response.blank?
|
||||
|
||||
return redirect_with_error("You don't have access to #{@upn}.") unless mailbox_accessible?
|
||||
|
||||
channel, inbox = create_channel_and_inbox
|
||||
redirect_to app_email_inbox_agents_url(account_id: @account.id, inbox_id: inbox.id)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_with_error('OAuth callback failed.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def exchange_code_for_tokens
|
||||
microsoft_shared_client.auth_code.get_token(
|
||||
params[:code],
|
||||
redirect_uri: "#{base_url}/microsoft_shared/callback"
|
||||
)
|
||||
end
|
||||
|
||||
def mailbox_accessible?
|
||||
response = Faraday.new(url: GRAPH_BASE_URL).get("/users/#{CGI.escape(@upn)}/mailFolders/inbox") do |req|
|
||||
req.headers['Authorization'] = "Bearer #{access_token}"
|
||||
end
|
||||
response.status == 200
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Microsoft Shared mailbox access check failed: #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def create_channel_and_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = Channel::Email.create!(
|
||||
account: @account,
|
||||
email: @upn,
|
||||
provider: 'microsoft_shared',
|
||||
provider_config: provider_config_payload
|
||||
)
|
||||
inbox = @account.inboxes.create!(account: @account, channel: channel, name: @upn)
|
||||
[channel, inbox]
|
||||
end
|
||||
end
|
||||
|
||||
def provider_config_payload
|
||||
{
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
expires_on: expires_on,
|
||||
oauth_user_email: oauth_user_email
|
||||
}
|
||||
end
|
||||
|
||||
def access_token
|
||||
parsed_body['access_token']
|
||||
end
|
||||
|
||||
def refresh_token
|
||||
parsed_body['refresh_token']
|
||||
end
|
||||
|
||||
def expires_on
|
||||
expires_in = parsed_body['expires_in']
|
||||
return (Time.current.utc + 1.hour).to_s if expires_in.blank?
|
||||
|
||||
(Time.current.utc + expires_in.to_i.seconds).to_s
|
||||
end
|
||||
|
||||
def oauth_user_email
|
||||
id_token = parsed_body['id_token']
|
||||
return nil if id_token.blank?
|
||||
|
||||
decoded = JWT.decode(id_token, nil, false).first
|
||||
decoded['email'].presence || decoded['preferred_username'].presence || decoded['upn'].presence
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
def redirect_with_error(message)
|
||||
Rails.logger.warn("MicrosoftShared callback rejected: #{message}")
|
||||
redirect_to '/'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class MicrosoftSharedClient extends ApiClient {
|
||||
constructor() {
|
||||
super('microsoft_shared', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization(payload) {
|
||||
return axios.post(`${this.url}/authorization`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default new MicrosoftSharedClient();
|
||||
@@ -524,6 +524,16 @@
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"MICROSOFT_SHARED": {
|
||||
"TITLE": "Microsoft Shared Inbox",
|
||||
"DESCRIPTION": "Connect a shared mailbox (e.g. sales@yourcompany.com) using a Microsoft 365 user account that has Send As and Full Access permissions on the mailbox. All sending and receiving uses Microsoft Graph API, so it works with Security Defaults and survives the SMTP Basic Auth retirement.",
|
||||
"UPN_LABEL": "Shared mailbox address",
|
||||
"UPN_PLACEHOLDER": "sales@yourcompany.com",
|
||||
"UPN_HELP": "The address of the shared mailbox you want to add. You'll authenticate next as a delegate user.",
|
||||
"SIGN_IN": "Continue with Microsoft",
|
||||
"INVALID_UPN": "Enter a valid email address.",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
@@ -1131,6 +1141,10 @@
|
||||
"TITLE": "Microsoft",
|
||||
"DESCRIPTION": "Connect with Microsoft"
|
||||
},
|
||||
"MICROSOFT_SHARED": {
|
||||
"TITLE": "Microsoft Shared Inbox",
|
||||
"DESCRIPTION": "Connect a Microsoft 365 shared mailbox via Graph API"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google",
|
||||
"DESCRIPTION": "Connect with Google"
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import ForwardToOption from './emailChannels/ForwardToOption.vue';
|
||||
import Microsoft from './emailChannels/Microsoft.vue';
|
||||
// === LeadChat: microsoft_shared (start) ===
|
||||
import MicrosoftShared from './emailChannels/MicrosoftShared.vue';
|
||||
// === LeadChat: microsoft_shared (end) ===
|
||||
import Google from './emailChannels/Google.vue';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
@@ -26,6 +29,15 @@ const emailProviderList = computed(() => {
|
||||
key: 'microsoft',
|
||||
icon: 'i-woot-outlook',
|
||||
},
|
||||
// === LeadChat: microsoft_shared (start) ===
|
||||
{
|
||||
title: t('INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT_SHARED.TITLE'),
|
||||
description: t('INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT_SHARED.DESCRIPTION'),
|
||||
isEnabled: !!globalConfig.value.azureAppId,
|
||||
key: 'microsoft_shared',
|
||||
icon: 'i-woot-outlook',
|
||||
},
|
||||
// === LeadChat: microsoft_shared (end) ===
|
||||
{
|
||||
title: t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE.TITLE'),
|
||||
description: t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE.DESCRIPTION'),
|
||||
@@ -75,6 +87,9 @@ function onClick(emailProvider) {
|
||||
</div>
|
||||
</div>
|
||||
<Microsoft v-else-if="provider === 'microsoft'" />
|
||||
<!-- === LeadChat: microsoft_shared (start) === -->
|
||||
<MicrosoftShared v-else-if="provider === 'microsoft_shared'" />
|
||||
<!-- === LeadChat: microsoft_shared (end) === -->
|
||||
<Google v-else-if="provider === 'google'" />
|
||||
<ForwardToOption v-else-if="provider === 'other_provider'" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import microsoftSharedClient from 'dashboard/api/channel/microsoftSharedClient';
|
||||
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineOptions({
|
||||
name: 'MicrosoftSharedChannel',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const sharedMailboxUpn = ref('');
|
||||
const isRequestingAuthorization = ref(false);
|
||||
|
||||
const trimmedUpn = computed(() => sharedMailboxUpn.value.trim().toLowerCase());
|
||||
|
||||
const isValidUpn = computed(() => {
|
||||
if (!trimmedUpn.value) return false;
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedUpn.value);
|
||||
});
|
||||
|
||||
async function requestAuthorization() {
|
||||
if (!isValidUpn.value) {
|
||||
useAlert(t('INBOX_MGMT.ADD.MICROSOFT_SHARED.INVALID_UPN'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await microsoftSharedClient.generateAuthorization({
|
||||
shared_mailbox_upn: trimmedUpn.value,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.ADD.MICROSOFT_SHARED.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isRequestingAuthorization.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full p-6 col-span-6">
|
||||
<SettingsSubPageHeader
|
||||
:header-title="t('INBOX_MGMT.ADD.MICROSOFT_SHARED.TITLE')"
|
||||
:header-content="t('INBOX_MGMT.ADD.MICROSOFT_SHARED.DESCRIPTION')"
|
||||
/>
|
||||
<form class="mt-6 max-w-xl" @submit.prevent="requestAuthorization">
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-200 mb-2">
|
||||
{{ t('INBOX_MGMT.ADD.MICROSOFT_SHARED.UPN_LABEL') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="sharedMailboxUpn"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="off"
|
||||
:placeholder="t('INBOX_MGMT.ADD.MICROSOFT_SHARED.UPN_PLACEHOLDER')"
|
||||
class="w-full rounded border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-woot-500"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ t('INBOX_MGMT.ADD.MICROSOFT_SHARED.UPN_HELP') }}
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<NextButton
|
||||
:is-loading="isRequestingAuthorization"
|
||||
:disabled="!isValidUpn"
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:label="t('INBOX_MGMT.ADD.MICROSOFT_SHARED.SIGN_IN')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -111,3 +111,7 @@ module ConversationReplyMailerHelper
|
||||
email.present? ? email.split('@').last : raise(StandardError, 'Channel email domain not present.')
|
||||
end
|
||||
end
|
||||
|
||||
# === LeadChat: microsoft_shared (start) ===
|
||||
ConversationReplyMailerHelper.prepend_mod_with('Leadchat::ConversationReplyMailerHelperExtension')
|
||||
# === LeadChat: microsoft_shared (end) ===
|
||||
|
||||
24
app/mailers/microsoft_shared_delivery.rb
Normal file
24
app/mailers/microsoft_shared_delivery.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class MicrosoftSharedDelivery
|
||||
attr_reader :settings
|
||||
|
||||
def initialize(settings)
|
||||
@settings = settings
|
||||
end
|
||||
|
||||
def deliver!(mail_message)
|
||||
channel = resolve_channel
|
||||
Microsoft::Shared::SendMailService.new(channel: channel, mail_message: mail_message).perform
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("MicrosoftSharedDelivery failed: channel_id=#{settings[:channel_id]} - #{e.class}: #{e.message}")
|
||||
raise
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_channel
|
||||
channel_id = settings[:channel_id]
|
||||
raise ArgumentError, 'MicrosoftSharedDelivery requires :channel_id in delivery_method_options' if channel_id.blank?
|
||||
|
||||
Channel::Email.find(channel_id)
|
||||
end
|
||||
end
|
||||
41
app/services/microsoft/shared/send_mail_service.rb
Normal file
41
app/services/microsoft/shared/send_mail_service.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class Microsoft::Shared::SendMailService
|
||||
pattr_initialize [:channel!, :mail_message!]
|
||||
|
||||
GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0'.freeze
|
||||
|
||||
def perform
|
||||
response = http_client.post(send_mail_path) do |req|
|
||||
req.headers['Authorization'] = "Bearer #{access_token}"
|
||||
req.headers['Content-Type'] = 'text/plain'
|
||||
req.body = encoded_mime
|
||||
end
|
||||
|
||||
raise SendMailError, "Microsoft Graph send failed: HTTP #{response.status} - #{response.body}" unless response.status == 202
|
||||
|
||||
Rails.logger.info("Microsoft Graph mail sent: channel=#{channel.id} upn=#{channel.email} subject=#{mail_message.subject.inspect}")
|
||||
response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access_token
|
||||
Microsoft::RefreshOauthTokenService.new(channel: channel).access_token
|
||||
end
|
||||
|
||||
def send_mail_path
|
||||
"/users/#{CGI.escape(channel.email)}/sendMail"
|
||||
end
|
||||
|
||||
def encoded_mime
|
||||
Base64.strict_encode64(mail_message.to_s)
|
||||
end
|
||||
|
||||
def http_client
|
||||
@http_client ||= Faraday.new(url: GRAPH_BASE_URL) do |f|
|
||||
f.options.open_timeout = 15
|
||||
f.options.timeout = 30
|
||||
end
|
||||
end
|
||||
|
||||
class SendMailError < StandardError; end
|
||||
end
|
||||
@@ -20,6 +20,9 @@ Rails.application.routes.draw do
|
||||
get '/app/*params', to: 'dashboard#index'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_inbox'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/microsoft', to: 'dashboard#index', as: 'app_new_microsoft_inbox'
|
||||
# === LeadChat: microsoft_shared (start) ===
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/microsoft_shared', to: 'dashboard#index', as: 'app_new_microsoft_shared_inbox'
|
||||
# === LeadChat: microsoft_shared (end) ===
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/instagram', to: 'dashboard#index', as: 'app_new_instagram_inbox'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/tiktok', to: 'dashboard#index', as: 'app_new_tiktok_inbox'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
|
||||
@@ -283,6 +286,12 @@ Rails.application.routes.draw do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
|
||||
# === LeadChat: microsoft_shared (start) ===
|
||||
namespace :microsoft_shared do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
# === LeadChat: microsoft_shared (end) ===
|
||||
|
||||
namespace :google do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
@@ -602,6 +611,9 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
get 'microsoft/callback', to: 'microsoft/callbacks#show'
|
||||
# === LeadChat: microsoft_shared (start) ===
|
||||
get 'microsoft_shared/callback', to: 'microsoft_shared/callbacks#show'
|
||||
# === LeadChat: microsoft_shared (end) ===
|
||||
get 'google/callback', to: 'google/callbacks#show'
|
||||
get 'instagram/callback', to: 'instagram/callbacks#show'
|
||||
get 'tiktok/callback', to: 'tiktok/callbacks#show'
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
module Custom::Leadchat::ConversationReplyMailerHelperExtension
|
||||
# Override the OAuth SMTP routing path so microsoft_shared channels are sent
|
||||
# via the Microsoft Graph API (custom :microsoft_shared delivery method)
|
||||
# instead of falling through to xoauth2 SMTP, which Microsoft Security
|
||||
# Defaults block tenant-wide.
|
||||
def oauth_smtp_settings
|
||||
if @channel.respond_to?(:microsoft_shared?) && @channel&.microsoft_shared?
|
||||
@options[:delivery_method] = :microsoft_shared
|
||||
@options[:delivery_method_options] = { channel_id: @channel.id }
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
4
custom/config/initializers/microsoft_shared_delivery.rb
Normal file
4
custom/config/initializers/microsoft_shared_delivery.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
Rails.application.configure do
|
||||
config.action_mailer.delivery_methods ||= {}
|
||||
ActionMailer::Base.add_delivery_method :microsoft_shared, MicrosoftSharedDelivery
|
||||
end
|
||||
Reference in New Issue
Block a user