Merge branch 'develop' into fix/CW-7007
This commit is contained in:
@@ -195,10 +195,10 @@ gem 'reverse_markdown'
|
||||
|
||||
gem 'iso-639'
|
||||
gem 'ruby-openai'
|
||||
gem 'ai-agents', '>= 0.9.1'
|
||||
gem 'ai-agents', '>= 0.10.0'
|
||||
|
||||
# TODO: Move this gem as a dependency of ai-agents
|
||||
gem 'ruby_llm', '>= 1.8.2'
|
||||
gem 'ruby_llm', '>= 1.14.1'
|
||||
gem 'ruby_llm-schema'
|
||||
|
||||
gem 'cld3', '~> 3.7'
|
||||
|
||||
+12
-12
@@ -126,8 +126,8 @@ GEM
|
||||
jbuilder (~> 2)
|
||||
rails (>= 4.2, < 7.2)
|
||||
selectize-rails (~> 0.6)
|
||||
ai-agents (0.9.1)
|
||||
ruby_llm (~> 1.9.1)
|
||||
ai-agents (0.10.0)
|
||||
ruby_llm (~> 1.14)
|
||||
annotaterb (4.20.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
@@ -307,8 +307,8 @@ GEM
|
||||
faraday-mashify (1.0.0)
|
||||
faraday (~> 2.0)
|
||||
hashie
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
@@ -466,7 +466,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.19.2)
|
||||
json (2.19.5)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
@@ -835,17 +835,17 @@ GEM
|
||||
ruby2ruby (2.5.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_llm (1.9.2)
|
||||
ruby_llm (1.15.0)
|
||||
base64
|
||||
event_stream_parser (~> 1)
|
||||
faraday (>= 1.10.0)
|
||||
faraday-multipart (>= 1)
|
||||
faraday-net_http (>= 1)
|
||||
faraday-retry (>= 1)
|
||||
marcel (~> 1.0)
|
||||
ruby_llm-schema (~> 0.2.1)
|
||||
marcel (~> 1)
|
||||
ruby_llm-schema (~> 0)
|
||||
zeitwerk (~> 2)
|
||||
ruby_llm-schema (0.2.5)
|
||||
ruby_llm-schema (0.3.0)
|
||||
ruby_parser (3.20.0)
|
||||
sexp_processor (~> 4.16)
|
||||
sass (3.7.4)
|
||||
@@ -1019,7 +1019,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -1039,7 +1039,7 @@ DEPENDENCIES
|
||||
administrate (>= 0.20.1)
|
||||
administrate-field-active_storage (>= 1.0.3)
|
||||
administrate-field-belongs_to_search (>= 0.9.0)
|
||||
ai-agents (>= 0.9.1)
|
||||
ai-agents (>= 0.10.0)
|
||||
annotaterb
|
||||
attr_extras
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
@@ -1144,7 +1144,7 @@ DEPENDENCIES
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-openai
|
||||
ruby_llm (>= 1.8.2)
|
||||
ruby_llm (>= 1.14.1)
|
||||
ruby_llm-schema
|
||||
scout_apm
|
||||
scss_lint
|
||||
|
||||
@@ -17,15 +17,12 @@ module Api::V1::InboxesHelper
|
||||
def validate_imap(channel_data)
|
||||
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
|
||||
|
||||
Mail.defaults do
|
||||
retriever_method :imap, { address: channel_data[:imap_address],
|
||||
port: channel_data[:imap_port],
|
||||
user_name: channel_data[:imap_login],
|
||||
password: channel_data[:imap_password],
|
||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||
end
|
||||
# Validate the user-selected auth mechanism before opening the connection.
|
||||
authentication = Imap::Authentication.validate_user_configurable!(channel_data[:imap_authentication])
|
||||
|
||||
check_imap_connection(channel_data)
|
||||
# Use the same auth adapter as the fetch service so LOGIN uses the IMAP LOGIN command,
|
||||
# not SASL AUTH=LOGIN.
|
||||
check_imap_connection(channel_data, authentication)
|
||||
end
|
||||
|
||||
def validate_smtp(channel_data)
|
||||
@@ -37,8 +34,8 @@ module Api::V1::InboxesHelper
|
||||
check_smtp_connection(channel_data, smtp)
|
||||
end
|
||||
|
||||
def check_imap_connection(channel_data)
|
||||
Mail.connection {} # rubocop:disable:block
|
||||
def check_imap_connection(channel_data, authentication)
|
||||
imap = open_imap_connection(channel_data, authentication)
|
||||
rescue SocketError => e
|
||||
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
|
||||
rescue Net::IMAP::NoResponseError => e
|
||||
@@ -53,9 +50,20 @@ module Api::V1::InboxesHelper
|
||||
rescue StandardError => e
|
||||
raise StandardError, e.message
|
||||
ensure
|
||||
imap.disconnect if imap.present? && !imap.disconnected?
|
||||
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
|
||||
end
|
||||
|
||||
def open_imap_connection(channel_data, authentication)
|
||||
imap = build_imap_connection(channel_data)
|
||||
Imap::Authentication.authenticate!(imap, authentication, channel_data[:imap_login], channel_data[:imap_password])
|
||||
imap
|
||||
end
|
||||
|
||||
def build_imap_connection(channel_data)
|
||||
Net::IMAP.new(channel_data[:imap_address], port: channel_data[:imap_port], ssl: channel_data[:imap_enable_ssl])
|
||||
end
|
||||
|
||||
def check_smtp_connection(channel_data, smtp)
|
||||
smtp.open_timeout = 10
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||
|
||||
@@ -1011,7 +1011,8 @@
|
||||
"LABEL": "Password",
|
||||
"PLACE_HOLDER": "Password"
|
||||
},
|
||||
"ENABLE_SSL": "Enable SSL"
|
||||
"ENABLE_SSL": "Enable SSL",
|
||||
"AUTH_MECHANISM": "Authentication"
|
||||
},
|
||||
"MICROSOFT": {
|
||||
"TITLE": "Microsoft",
|
||||
|
||||
@@ -5,11 +5,13 @@ import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFie
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import SingleSelectDropdown from './components/SingleSelectDropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsFieldSection,
|
||||
NextButton,
|
||||
SingleSelectDropdown,
|
||||
},
|
||||
props: {
|
||||
inbox: {
|
||||
@@ -28,6 +30,12 @@ export default {
|
||||
login: '',
|
||||
password: '',
|
||||
isSSLEnabled: true,
|
||||
authMechanism: 'plain',
|
||||
authMechanisms: [
|
||||
{ key: 1, value: 'plain' },
|
||||
{ key: 2, value: 'login' },
|
||||
{ key: 3, value: 'cram-md5' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
@@ -56,6 +64,7 @@ export default {
|
||||
imap_login,
|
||||
imap_password,
|
||||
imap_enable_ssl,
|
||||
imap_authentication,
|
||||
} = this.inbox;
|
||||
this.isIMAPEnabled = imap_enabled;
|
||||
this.address = imap_address;
|
||||
@@ -63,6 +72,7 @@ export default {
|
||||
this.login = imap_login;
|
||||
this.password = imap_password;
|
||||
this.isSSLEnabled = imap_enable_ssl;
|
||||
this.authMechanism = imap_authentication || 'plain';
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
@@ -77,6 +87,7 @@ export default {
|
||||
imap_login: this.login,
|
||||
imap_password: this.password,
|
||||
imap_enable_ssl: this.isSSLEnabled,
|
||||
imap_authentication: this.authMechanism,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -90,6 +101,9 @@ export default {
|
||||
useAlert(error.message);
|
||||
}
|
||||
},
|
||||
handleAuthMechanismChange(mode) {
|
||||
this.authMechanism = mode;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -155,6 +169,13 @@ export default {
|
||||
/>
|
||||
{{ $t('INBOX_MGMT.IMAP.ENABLE_SSL') }}
|
||||
</label>
|
||||
<SingleSelectDropdown
|
||||
class="w-full"
|
||||
:label="$t('INBOX_MGMT.IMAP.AUTH_MECHANISM')"
|
||||
:selected="authMechanism"
|
||||
:options="authMechanisms"
|
||||
:action="handleAuthMechanismChange"
|
||||
/>
|
||||
</div>
|
||||
<NextButton
|
||||
type="submit"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# email :string not null
|
||||
# forward_to_email :string not null
|
||||
# imap_address :string default("")
|
||||
# imap_authentication :string default("plain")
|
||||
# imap_enable_ssl :boolean default(TRUE)
|
||||
# imap_enabled :boolean default(FALSE)
|
||||
# imap_login :string default("")
|
||||
@@ -47,7 +48,7 @@ class Channel::Email < ApplicationRecord
|
||||
end
|
||||
|
||||
self.table_name = 'channel_email'
|
||||
EDITABLE_ATTRS = [:email, :imap_enabled, :imap_login, :imap_password, :imap_address, :imap_port, :imap_enable_ssl,
|
||||
EDITABLE_ATTRS = [:email, :imap_enabled, :imap_login, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_authentication,
|
||||
:smtp_enabled, :smtp_login, :smtp_password, :smtp_address, :smtp_port, :smtp_domain, :smtp_enable_starttls_auto,
|
||||
:smtp_enable_ssl_tls, :smtp_openssl_verify_mode, :smtp_authentication, :provider, :verified_for_sending].freeze
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
module Imap::Authentication
|
||||
DEFAULT_MECHANISM = 'plain'.freeze
|
||||
USER_CONFIGURABLE_MECHANISMS = %w[plain login cram-md5].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def normalize(mechanism)
|
||||
mechanism.presence || DEFAULT_MECHANISM
|
||||
end
|
||||
|
||||
def validate_user_configurable!(mechanism)
|
||||
normalized_mechanism = normalize(mechanism).to_s.downcase
|
||||
return normalized_mechanism if USER_CONFIGURABLE_MECHANISMS.include?(normalized_mechanism)
|
||||
|
||||
allowed_values = USER_CONFIGURABLE_MECHANISMS.join(', ')
|
||||
raise StandardError, "Invalid IMAP authentication mechanism. Allowed values: #{allowed_values}"
|
||||
end
|
||||
|
||||
def authenticate!(imap, mechanism, username, password)
|
||||
normalized_mechanism = normalize(mechanism).to_s.downcase
|
||||
|
||||
case normalized_mechanism
|
||||
when 'cram-md5'
|
||||
imap.authenticate('CRAM-MD5', username, password)
|
||||
when 'login'
|
||||
imap.login(username, password)
|
||||
else
|
||||
imap.authenticate(normalize(mechanism), username, password)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -130,8 +130,9 @@ class Imap::BaseFetchEmailService
|
||||
end
|
||||
|
||||
def build_imap_client
|
||||
imap = Net::IMAP.new(channel.imap_address, port: channel.imap_port, ssl: true)
|
||||
imap.authenticate(authentication_type, channel.imap_login, imap_password)
|
||||
imap = Net::IMAP.new(channel.imap_address, port: channel.imap_port, ssl: channel.imap_enable_ssl)
|
||||
Imap::Authentication.authenticate!(imap, authentication_type, channel.imap_login, imap_password)
|
||||
|
||||
imap.select('INBOX')
|
||||
imap
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Imap::FetchEmailService < Imap::BaseFetchEmailService
|
||||
private
|
||||
|
||||
def authentication_type
|
||||
'PLAIN'
|
||||
channel.imap_authentication || 'plain'
|
||||
end
|
||||
|
||||
def imap_password
|
||||
|
||||
@@ -90,6 +90,7 @@ if resource.email?
|
||||
json.imap_port resource.channel.try(:imap_port)
|
||||
json.imap_enabled resource.channel.try(:imap_enabled)
|
||||
json.imap_enable_ssl resource.channel.try(:imap_enable_ssl)
|
||||
json.imap_authentication resource.channel.try(:imap_authentication)
|
||||
|
||||
if resource.channel.try(:microsoft?) || resource.channel.try(:google?) || resource.channel.try(:legacy_google?)
|
||||
json.reauthorization_required resource.channel.try(:provider_config).empty? || resource.channel.try(:reauthorization_required?)
|
||||
|
||||
+39788
-2540
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
class AddImapAuthenticationToChannelEmail < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :channel_email, :imap_authentication, :string, default: 'plain'
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_04_30_114500) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_05_07_000000) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -472,6 +472,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_30_114500) do
|
||||
t.boolean "smtp_enable_ssl_tls", default: false
|
||||
t.jsonb "provider_config", default: {}
|
||||
t.string "provider"
|
||||
t.string "imap_authentication", default: "plain"
|
||||
t.boolean "verified_for_sending", default: false, null: false
|
||||
t.index ["email"], name: "index_channel_email_on_email", unique: true
|
||||
t.index ["forward_to_email"], name: "index_channel_email_on_forward_to_email", unique: true
|
||||
|
||||
@@ -568,8 +568,10 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
imap_connection = double
|
||||
allow(Mail).to receive(:connection).and_return(imap_connection)
|
||||
imap_connection = instance_double(Net::IMAP, disconnected?: false)
|
||||
allow(Net::IMAP).to receive(:new).and_return(imap_connection)
|
||||
allow(imap_connection).to receive(:login)
|
||||
allow(imap_connection).to receive(:disconnect)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -578,7 +580,8 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
imap_enabled: true,
|
||||
imap_address: 'imap.gmail.com',
|
||||
imap_port: 993,
|
||||
imap_login: 'imaptest@gmail.com'
|
||||
imap_login: 'imaptest@gmail.com',
|
||||
imap_authentication: 'login'
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
@@ -587,6 +590,7 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(email_channel.reload.imap_enabled).to be true
|
||||
expect(email_channel.reload.imap_address).to eq('imap.gmail.com')
|
||||
expect(email_channel.reload.imap_port).to eq(993)
|
||||
expect(email_channel.reload.imap_authentication).to eq('login')
|
||||
end
|
||||
|
||||
it 'updates avatar when administrator' do
|
||||
|
||||
@@ -16,6 +16,7 @@ FactoryBot.define do
|
||||
imap_login { 'email@example.com' }
|
||||
imap_password { '' }
|
||||
imap_enable_ssl { true }
|
||||
|
||||
provider_config do
|
||||
{
|
||||
expires_on: Time.zone.now + 3600,
|
||||
@@ -33,6 +34,7 @@ FactoryBot.define do
|
||||
imap_login { 'email@example.com' }
|
||||
imap_password { 'random-password' }
|
||||
imap_enable_ssl { true }
|
||||
imap_authentication { 'plain' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,14 +13,68 @@ RSpec.describe Imap::FetchEmailService do
|
||||
before do
|
||||
allow(Rails).to receive(:logger).and_return(logger)
|
||||
allow(Net::IMAP).to receive(:new).with(
|
||||
imap_email_channel.imap_address, port: imap_email_channel.imap_port, ssl: true
|
||||
imap_email_channel.imap_address, port: imap_email_channel.imap_port, ssl: imap_email_channel.imap_enable_ssl
|
||||
).and_return(imap)
|
||||
allow(imap).to receive(:authenticate).with(
|
||||
'PLAIN', imap_email_channel.imap_login, imap_email_channel.imap_password
|
||||
'plain', imap_email_channel.imap_login, imap_email_channel.imap_password
|
||||
)
|
||||
allow(imap).to receive(:select).with('INBOX')
|
||||
end
|
||||
|
||||
context 'when using CRAM-MD5 authentication' do
|
||||
let(:cram_md5_channel) { create(:channel_email, :imap_email, account: account, imap_authentication: 'cram-md5') }
|
||||
|
||||
before do
|
||||
allow(Net::IMAP).to receive(:new).with(
|
||||
cram_md5_channel.imap_address, port: cram_md5_channel.imap_port, ssl: cram_md5_channel.imap_enable_ssl
|
||||
).and_return(imap)
|
||||
allow(imap).to receive(:authenticate).with(
|
||||
'CRAM-MD5', cram_md5_channel.imap_login, cram_md5_channel.imap_password
|
||||
)
|
||||
allow(imap).to receive(:select).with('INBOX')
|
||||
end
|
||||
|
||||
it 'uses CRAM-MD5 authentication' do
|
||||
travel_to '26.10.2020 10:00'.to_datetime do
|
||||
allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([])
|
||||
allow(imap).to receive(:logout)
|
||||
|
||||
described_class.new(channel: cram_md5_channel).perform
|
||||
|
||||
expect(imap).to have_received(:authenticate).with(
|
||||
'CRAM-MD5', cram_md5_channel.imap_login, cram_md5_channel.imap_password
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using LOGIN authentication' do
|
||||
let(:login_channel) { create(:channel_email, :imap_email, account: account, imap_authentication: 'login') }
|
||||
|
||||
before do
|
||||
allow(Net::IMAP).to receive(:new).with(
|
||||
login_channel.imap_address, port: login_channel.imap_port, ssl: login_channel.imap_enable_ssl
|
||||
).and_return(imap)
|
||||
allow(imap).to receive(:login).with(
|
||||
login_channel.imap_login, login_channel.imap_password
|
||||
)
|
||||
allow(imap).to receive(:select).with('INBOX')
|
||||
end
|
||||
|
||||
it 'uses LOGIN authentication' do
|
||||
travel_to '26.10.2020 10:00'.to_datetime do
|
||||
allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([])
|
||||
allow(imap).to receive(:logout)
|
||||
|
||||
described_class.new(channel: login_channel).perform
|
||||
|
||||
expect(imap).to have_received(:login).with(
|
||||
login_channel.imap_login, login_channel.imap_password
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when new emails are available in the mailbox' do
|
||||
it 'fetches the emails and returns the emails that are not present in the db' do
|
||||
travel_to '26.10.2020 10:00'.to_datetime do
|
||||
|
||||
Reference in New Issue
Block a user