Merge branch 'develop' into fix/CW-7007

This commit is contained in:
Sivin Varghese
2026-05-08 19:54:38 +05:30
committed by GitHub
16 changed files with 39953 additions and 2575 deletions
+2 -2
View File
@@ -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
View File
@@ -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
+18 -10
View File
@@ -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"
+2 -1
View File
@@ -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
+31
View File
@@ -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
+1 -1
View File
@@ -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
View File
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
View File
@@ -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
+2
View File
@@ -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
+56 -2
View File
@@ -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