diff --git a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationsView.vue b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationsView.vue
index 39a1c6b58..058305a7a 100644
--- a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationsView.vue
+++ b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationsView.vue
@@ -11,6 +11,7 @@
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index c577b1908..22be5f036 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,6 +1,8 @@
class ApplicationJob < ActiveJob::Base
# https://api.rubyonrails.org/v5.2.1/classes/ActiveJob/Exceptions/ClassMethods.html
- discard_on ActiveJob::DeserializationError do |_job, error|
- Rails.logger.error("Skipping job because of ActiveJob::DeserializationError (#{error.message})")
+ discard_on ActiveJob::DeserializationError do |job, error|
+ Rails.logger.info("Skipping #{job.class} with #{
+ job.instance_variable_get(:@serialized_arguments)
+ } because of ActiveJob::DeserializationError (#{error.message})")
end
end
diff --git a/app/jobs/data_import_job.rb b/app/jobs/data_import_job.rb
index 0a973c8bd..9703d2e50 100644
--- a/app/jobs/data_import_job.rb
+++ b/app/jobs/data_import_job.rb
@@ -8,8 +8,12 @@ class DataImportJob < ApplicationJob
def perform(data_import)
@data_import = data_import
@contact_manager = DataImport::ContactManager.new(@data_import.account)
- process_import_file
- send_import_notification_to_admin
+ begin
+ process_import_file
+ send_import_notification_to_admin
+ rescue CSV::MalformedCSVError => e
+ handle_csv_error(e)
+ end
end
private
@@ -83,7 +87,16 @@ class DataImportJob < ApplicationJob
end
end
+ def handle_csv_error(error) # rubocop:disable Lint/UnusedMethodArgument
+ @data_import.update!(status: :failed)
+ send_import_failed_notification_to_admin
+ end
+
def send_import_notification_to_admin
AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_complete(@data_import).deliver_later
end
+
+ def send_import_failed_notification_to_admin
+ AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_failed.deliver_later
+ end
end
diff --git a/app/jobs/inboxes/fetch_imap_emails_job.rb b/app/jobs/inboxes/fetch_imap_emails_job.rb
index 8d3582440..c59ae2787 100644
--- a/app/jobs/inboxes/fetch_imap_emails_job.rb
+++ b/app/jobs/inboxes/fetch_imap_emails_job.rb
@@ -1,17 +1,21 @@
require 'net/imap'
-class Inboxes::FetchImapEmailsJob < ApplicationJob
+class Inboxes::FetchImapEmailsJob < MutexApplicationJob
queue_as :scheduled_jobs
def perform(channel)
return unless should_fetch_email?(channel)
- process_email_for_channel(channel)
+ with_lock(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id) do
+ process_email_for_channel(channel)
+ end
rescue *ExceptionList::IMAP_EXCEPTIONS => e
Rails.logger.error e
channel.authorization_error!
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError => e
Rails.logger.error e
+ rescue LockAcquisitionError
+ Rails.logger.error "Lock failed for #{channel.inbox.id}"
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
end
@@ -23,7 +27,6 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
end
def process_email_for_channel(channel)
- # fetching email for microsoft provider
if channel.microsoft?
fetch_mail_for_ms_provider(channel)
else
@@ -34,32 +37,66 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
end
def fetch_mail_for_channel(channel)
- imap_inbox = authenticated_imap_inbox(channel, channel.imap_password, 'PLAIN')
- last_email_time = DateTime.parse(Net::IMAP.format_datetime(last_email_time(channel)))
+ imap_client = build_imap_client(channel, channel.imap_password, 'PLAIN')
- received_mails(imap_inbox).each do |message_id|
- inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
-
- mail_info_logger(channel, inbound_mail, message_id)
-
- next if email_already_present?(channel, inbound_mail, last_email_time)
-
- process_mail(inbound_mail, channel)
+ message_ids_with_seq = fetch_message_ids_with_sequence(imap_client, channel)
+ message_ids_with_seq.each do |message_id_with_seq|
+ process_message_id(channel, imap_client, message_id_with_seq)
end
end
- def email_already_present?(channel, inbound_mail, _last_email_time)
- channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
+ def process_message_id(channel, imap_client, message_id_with_seq)
+ seq_no, message_id = message_id_with_seq
+
+ return if email_already_present?(channel, message_id)
+
+ # Fetch the original mail content using the sequence no
+ mail_str = imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']
+
+ if mail_str.blank?
+ Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetch failed for #{channel.email} with message-id <#{message_id}>."
+ return
+ end
+
+ inbound_mail = build_mail_from_string(mail_str)
+ mail_info_logger(channel, inbound_mail, seq_no)
+ process_mail(inbound_mail, channel)
end
- def received_mails(imap_inbox)
- imap_inbox.search(['BEFORE', tomorrow, 'SINCE', yesterday])
+ # Sends a FETCH command to retrieve data associated with a message in the mailbox.
+ # You can send batches of message sequence number in `.fetch` method.
+ def fetch_message_ids_with_sequence(imap_client, channel)
+ seq_nums = fetch_available_mail_sequence_numbers(imap_client)
+
+ Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{channel.email}, found #{seq_nums.length}."
+
+ message_ids_with_seq = []
+ seq_nums.each_slice(10).each do |batch|
+ # Fetch only message-id only without mail body or contents.
+ batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)]')
+
+ # .fetch returns an array of Net::IMAP::FetchData or nil
+ # (instead of an empty array) if there is no matching message.
+ # Check
+ if batch_message_ids.blank?
+ Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}."
+ next
+ end
+
+ batch_message_ids.each do |data|
+ message_id = build_mail_from_string(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']).message_id
+ message_ids_with_seq.push([data.seqno, message_id])
+ end
+ end
+
+ message_ids_with_seq
end
- def processed_email?(current_email, last_email_time)
- return current_email.date < last_email_time if current_email.date.present?
-
- false
+ # Sends a SEARCH command to search the mailbox for messages that were
+ # created between yesterday and today and returns message sequence numbers.
+ # Return
+ def fetch_available_mail_sequence_numbers(imap_client)
+ imap_client.search(['BEFORE', tomorrow, 'SINCE', yesterday])
end
def fetch_mail_for_ms_provider(channel)
@@ -69,16 +106,15 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
return unless access_token
- imap_inbox = authenticated_imap_inbox(channel, access_token, 'XOAUTH2')
-
- process_mails(imap_inbox, channel)
+ imap_client = build_imap_client(channel, access_token, 'XOAUTH2')
+ process_mails(imap_client, channel)
end
- def process_mails(imap_inbox, channel)
- received_mails(imap_inbox).each do |message_id|
- inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
+ def process_mails(imap_client, channel)
+ fetch_available_mail_sequence_numbers(imap_client).each do |seq_no|
+ inbound_mail = Mail.read_from_string imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']
- mail_info_logger(channel, inbound_mail, message_id)
+ mail_info_logger(channel, inbound_mail, seq_no)
next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
@@ -86,38 +122,26 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
end
end
- def mail_info_logger(channel, inbound_mail, message_id)
+ def mail_info_logger(channel, inbound_mail, uid)
return if Rails.env.test?
Rails.logger.info("
- #{channel.provider} Email id: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}, message_id: #{message_id}")
+ #{channel.provider} Email id: #{inbound_mail.from} - message_source_id: #{inbound_mail.message_id} - sequence id: #{uid}")
end
- def authenticated_imap_inbox(channel, access_token, auth_method)
+ def build_imap_client(channel, access_token, auth_method)
imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true)
imap.authenticate(auth_method, channel.imap_login, access_token)
imap.select('INBOX')
imap
end
- def last_email_time(channel)
- # we are only checking for emails in last 2 day
- last_email_incoming_message = channel.inbox.messages.incoming.where('messages.created_at >= ?', 2.days.ago).last
- if last_email_incoming_message.present?
- time = last_email_incoming_message.content_attributes['email']['date']
- time ||= last_email_incoming_message.created_at.to_s
- end
- time ||= 1.hour.ago.to_s
-
- DateTime.parse(time)
+ def email_already_present?(channel, message_id)
+ channel.inbox.messages.find_by(source_id: message_id).present?
end
- def yesterday
- (Time.zone.today - 1).strftime('%d-%b-%Y')
- end
-
- def tomorrow
- (Time.zone.today + 1).strftime('%d-%b-%Y')
+ def build_mail_from_string(raw_email_content)
+ Mail.read_from_string(raw_email_content)
end
def process_mail(inbound_mail, channel)
@@ -132,4 +156,12 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
def valid_access_token(channel)
Microsoft::RefreshOauthTokenService.new(channel: channel).access_token
end
+
+ def yesterday
+ (Time.zone.today - 1).strftime('%d-%b-%Y')
+ end
+
+ def tomorrow
+ (Time.zone.today + 1).strftime('%d-%b-%Y')
+ end
end
diff --git a/app/jobs/mutex_application_job.rb b/app/jobs/mutex_application_job.rb
index d6d20b1e2..98e6bf9cd 100644
--- a/app/jobs/mutex_application_job.rb
+++ b/app/jobs/mutex_application_job.rb
@@ -18,17 +18,15 @@ class MutexApplicationJob < ApplicationJob
lock_key = format(key_format, *args)
lock_manager = Redis::LockManager.new
- if lock_manager.locked?(lock_key)
- Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
- raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
- end
-
begin
- lock_manager.lock(lock_key)
- Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
- yield
+ if lock_manager.lock(lock_key)
+ Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
+ yield
+ else
+ Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
+ raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
+ end
ensure
- # Ensure that the lock is released even if there's an error in processing
lock_manager.unlock(lock_key)
end
end
diff --git a/app/jobs/notification/remove_old_notification_job.rb b/app/jobs/notification/remove_old_notification_job.rb
new file mode 100644
index 000000000..770461744
--- /dev/null
+++ b/app/jobs/notification/remove_old_notification_job.rb
@@ -0,0 +1,8 @@
+class Notification::RemoveOldNotificationJob < ApplicationJob
+ queue_as :low
+
+ def perform
+ Notification.where('created_at < ?', 1.month.ago)
+ .find_each(batch_size: 1000, &:delete)
+ end
+end
diff --git a/app/jobs/trigger_scheduled_items_job.rb b/app/jobs/trigger_scheduled_items_job.rb
index 9fc7a6491..82634e4d5 100644
--- a/app/jobs/trigger_scheduled_items_job.rb
+++ b/app/jobs/trigger_scheduled_items_job.rb
@@ -19,5 +19,8 @@ class TriggerScheduledItemsJob < ApplicationJob
# Job to sync whatsapp templates
Channels::Whatsapp::TemplatesSyncSchedulerJob.perform_later
+
+ # Job to clear notifications which are older than 1 month
+ Notification::RemoveOldNotificationJob.perform_later
end
end
diff --git a/app/mailers/administrator_notifications/channel_notifications_mailer.rb b/app/mailers/administrator_notifications/channel_notifications_mailer.rb
index b25a6d66b..8c52a9bd3 100644
--- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb
+++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb
@@ -51,6 +51,15 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end
+ def contact_import_failed
+ return unless smtp_config_set_or_development?
+
+ subject = 'Contact Import Failed'
+
+ @meta = {}
+ send_mail_with_liquid(to: admin_emails, subject: subject) and return
+ end
+
def contact_export_complete(file_url)
return unless smtp_config_set_or_development?
diff --git a/app/models/channel/telegram.rb b/app/models/channel/telegram.rb
index 365ca59c8..f6b97f2d2 100644
--- a/app/models/channel/telegram.rb
+++ b/app/models/channel/telegram.rb
@@ -149,24 +149,32 @@ class Channel::Telegram < ApplicationRecord
})
end
- def convert_markdown_to_telegram(text)
- ## supported characters : https://core.telegram.org/bots/api#markdown-style
- ## To implement MarkdownV2, we will need to do a lot of escaping
+ def convert_markdown_to_telegram_html(text)
+ # ref: https://core.telegram.org/bots/api#html-style
- # Convert bold - double asterisks to single asterisk in Telegram
- # Chatwoot uses double asterisks for bold, while telegram used single asterisk
- text.gsub!(/\*\*(.*?)\*\*/, '*\1*')
- text
+ # escape html tags in text. We are subbing \n to since commonmark will strip exta '\n'
+ text = CGI.escapeHTML(text.gsub("\n", ' '))
+
+ # convert markdown to html
+ html = CommonMarker.render_html(text).strip
+
+ # remove all html tags except b, strong, i, em, u, ins, s, strike, del, a, code, pre, blockquote
+ stripped_html = Rails::HTML5::SafeListSanitizer.new.sanitize(html, tags: %w[b strong i em u ins s strike del a code pre blockquote],
+ attributes: %w[href])
+
+ # converted escaped br tags to \n
+ stripped_html.gsub('<br>', "\n")
end
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil)
- text_to_md = convert_markdown_to_telegram(text)
+ text_payload = convert_markdown_to_telegram_html(text)
+
HTTParty.post("#{telegram_api_url}/sendMessage",
body: {
chat_id: chat_id,
- text: text_to_md,
+ text: text_payload,
reply_markup: reply_markup,
- parse_mode: 'Markdown',
+ parse_mode: 'HTML',
reply_to_message_id: reply_to_message_id
})
end
diff --git a/app/models/dashboard_app.rb b/app/models/dashboard_app.rb
index 7316dd344..e8a7edd4c 100644
--- a/app/models/dashboard_app.rb
+++ b/app/models/dashboard_app.rb
@@ -33,9 +33,12 @@ class DashboardApp < ApplicationRecord
'required' => %w[url type],
'properties' => {
'type' => { 'enum': ['frame'] },
- 'url' => { :type => 'string', 'format' => 'uri' }
+ 'url' => { '$ref' => '#/definitions/saneUrl' }
}
},
+ 'definitions' => {
+ 'saneUrl' => { 'format' => 'uri', 'pattern' => '^https?://' }
+ },
'additionalProperties' => false,
'minItems' => 1
}
diff --git a/app/views/api/v1/accounts/notifications/index.json.jbuilder b/app/views/api/v1/accounts/notifications/index.json.jbuilder
index e04ce22cf..b46dcb9ec 100644
--- a/app/views/api/v1/accounts/notifications/index.json.jbuilder
+++ b/app/views/api/v1/accounts/notifications/index.json.jbuilder
@@ -19,6 +19,7 @@ json.data do
json.secondary_actor notification.secondary_actor&.push_event_data
json.user notification.user.push_event_data
json.created_at notification.created_at.to_i
+ json.last_activity_at notification.last_activity_at.to_i
end
end
end
diff --git a/app/views/icons/_chatwoot-logo.html.erb b/app/views/icons/_chatwoot-logo.html.erb
deleted file mode 100644
index 5377a0a02..000000000
--- a/app/views/icons/_chatwoot-logo.html.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_failed.liquid b/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_failed.liquid
new file mode 100644
index 000000000..835cff258
--- /dev/null
+++ b/app/views/mailers/administrator_notifications/channel_notifications_mailer/contact_import_failed.liquid
@@ -0,0 +1,3 @@
+
Hello,
+
+
Your contact import has failed. It appears that the CSV file you uploaded may not be valid. We kindly request that you review the file and ensure it complies with the required format.