Aggiunge IMAP bounce e flag email non valide nel CRM.
Permette di leggere i bounce dalla stessa MailIdentity SMTP, aggiornare le email in anagrafica e saltare gli indirizzi invalidi nei mailing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -147,8 +147,206 @@ module Crm
|
||||
end
|
||||
end
|
||||
|
||||
# Aggiorna email organizzazione/contatto: bounce, sostituzione, flag non valida + nota timeline.
|
||||
def update_organization_email(project_code, attrs)
|
||||
with_project(project_code) do
|
||||
attrs = attrs.to_h.symbolize_keys
|
||||
organization = organizations_scope.includes(:contacts).find(attrs[:organization_id])
|
||||
contact = nil
|
||||
if attrs[:contact_id].present?
|
||||
contact = organization.contacts.find(attrs[:contact_id])
|
||||
elsif attrs[:update_primary_contact] != false
|
||||
contact = organization.primary_contact || organization.contacts.order(:id).first
|
||||
end
|
||||
|
||||
bounced = attrs[:bounced_email].to_s.strip.downcase.presence
|
||||
new_email = attrs.key?(:email) ? attrs[:email].to_s.strip.downcase.presence : :unchanged
|
||||
mark_invalid = if attrs.key?(:email_invalid)
|
||||
ActiveModel::Type::Boolean.new.cast(attrs[:email_invalid])
|
||||
elsif bounced.present? && (new_email == :unchanged || new_email.nil? || new_email == bounced)
|
||||
true
|
||||
elsif new_email.is_a?(String) && new_email != bounced
|
||||
false
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
previous_org_email = organization.email
|
||||
previous_contact_email = contact&.email
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
org_changes = {}
|
||||
if bounced
|
||||
org_changes[:bounced_email] = bounced
|
||||
end
|
||||
unless new_email == :unchanged
|
||||
org_changes[:email] = new_email
|
||||
end
|
||||
unless mark_invalid.nil?
|
||||
org_changes[:email_invalid] = mark_invalid
|
||||
end
|
||||
if attrs[:website].present?
|
||||
org_changes[:website] = attrs[:website].to_s.strip
|
||||
end
|
||||
organization.update!(org_changes) if org_changes.any?
|
||||
|
||||
if contact
|
||||
contact_changes = {}
|
||||
if bounced
|
||||
contact_changes[:bounced_email] = bounced
|
||||
end
|
||||
unless new_email == :unchanged
|
||||
contact_changes[:email] = new_email
|
||||
end
|
||||
unless mark_invalid.nil?
|
||||
contact_changes[:email_invalid] = mark_invalid
|
||||
end
|
||||
contact.update!(contact_changes) if contact_changes.any?
|
||||
end
|
||||
|
||||
subject = attrs[:activity_subject].presence || begin
|
||||
if new_email.is_a?(String) && new_email.present? && new_email != bounced
|
||||
"Email aggiornata dopo bounce"
|
||||
else
|
||||
"Email non valida (bounce)"
|
||||
end
|
||||
end
|
||||
description = attrs[:activity_description].presence || build_email_update_description(
|
||||
bounced: bounced,
|
||||
new_email: new_email == :unchanged ? nil : new_email,
|
||||
previous_org_email: previous_org_email,
|
||||
previous_contact_email: previous_contact_email,
|
||||
reason: attrs[:bounce_reason]
|
||||
)
|
||||
|
||||
activity = organization.activities.create!(
|
||||
user: @user,
|
||||
contact: contact,
|
||||
activity_type: "note",
|
||||
subject: subject,
|
||||
description: description,
|
||||
happened_at: Time.current
|
||||
)
|
||||
|
||||
ok(
|
||||
organization: organization_json(organization.reload),
|
||||
contact: contact ? contact_json(contact.reload) : nil,
|
||||
activity: activity_json(activity)
|
||||
)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
err("Organizzazione o contatto non trovato", status: :not_found)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
validation_error(e.record)
|
||||
end
|
||||
end
|
||||
|
||||
# Legge bounce IMAP dall'account SMTP/IMAP (stesse credenziali MailIdentity).
|
||||
def check_mail_bounces(project_code = nil, attrs = {})
|
||||
attrs = attrs.to_h.symbolize_keys
|
||||
if project_code.present?
|
||||
project = Project.active.find_by(code: project_code.to_s)
|
||||
return err("Progetto non trovato", status: :not_found) if project.nil?
|
||||
return err("Progetto non accessibile", status: :forbidden) unless @user.can_access_project?(project)
|
||||
|
||||
@project = project
|
||||
Current.project = project
|
||||
end
|
||||
|
||||
identity = find_mail_identity(attrs)
|
||||
return err("Account email non trovato", status: :not_found) if identity.nil?
|
||||
|
||||
since_days = (attrs[:since_days].presence || 14).to_i.clamp(1, 90)
|
||||
result = Mailings::ImapBounceReader.new(
|
||||
identity,
|
||||
since: since_days.days.ago,
|
||||
mailbox: attrs[:mailbox].presence || "INBOX",
|
||||
limit: (attrs[:limit].presence || 200).to_i.clamp(1, 500)
|
||||
).call
|
||||
|
||||
unless result.ok
|
||||
return err(result.error || "Lettura IMAP fallita", status: :unprocessable_entity, extra: { identity: result.identity })
|
||||
end
|
||||
|
||||
bounces = enrich_bounces_with_crm(result.bounces)
|
||||
ok(
|
||||
identity: result.identity,
|
||||
since_days: since_days,
|
||||
scanned: result.scanned,
|
||||
bounce_count: bounces.size,
|
||||
unique_failed_emails: bounces.flat_map { |b| b[:failed_emails] }.uniq.sort,
|
||||
bounces: bounces
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_mail_identity(attrs)
|
||||
scope = MailIdentity.active
|
||||
if attrs[:mail_identity_id].present?
|
||||
scope.find_by(id: attrs[:mail_identity_id])
|
||||
elsif attrs[:from_email].present?
|
||||
scope.find_by("LOWER(from_email) = ?", attrs[:from_email].to_s.downcase.strip)
|
||||
else
|
||||
scope.imap_enabled.ordered.find_by("LOWER(from_email) = ?", "info@matchlivetv.it") ||
|
||||
scope.imap_enabled.ordered.first
|
||||
end
|
||||
end
|
||||
|
||||
def enrich_bounces_with_crm(bounces)
|
||||
return bounces if @project.nil?
|
||||
|
||||
orgs = organizations_scope.includes(:contacts).to_a
|
||||
bounces.map do |bounce|
|
||||
matches = []
|
||||
Array(bounce[:failed_emails]).each do |email|
|
||||
org = orgs.find { |o| o.email.to_s.downcase == email } ||
|
||||
orgs.find { |o| o.contacts.any? { |c| c.email.to_s.downcase == email } } ||
|
||||
orgs.find { |o| o.bounced_email.to_s.downcase == email }
|
||||
next unless org
|
||||
|
||||
matches << {
|
||||
organization_id: org.id,
|
||||
name: org.name,
|
||||
current_email: org.email,
|
||||
email_invalid: org.email_invalid?,
|
||||
sport: org.sport
|
||||
}
|
||||
end
|
||||
if matches.empty? && bounce[:club_hint].present?
|
||||
hint = bounce[:club_hint].to_s.downcase
|
||||
org = orgs.find { |o| o.name.to_s.downcase == hint } ||
|
||||
orgs.find { |o| o.name.to_s.downcase.include?(hint) || hint.include?(o.name.to_s.downcase) }
|
||||
if org
|
||||
matches << {
|
||||
organization_id: org.id,
|
||||
name: org.name,
|
||||
current_email: org.email,
|
||||
email_invalid: org.email_invalid?,
|
||||
sport: org.sport,
|
||||
matched_by: "club_hint"
|
||||
}
|
||||
end
|
||||
end
|
||||
bounce.merge(crm_matches: matches)
|
||||
end
|
||||
end
|
||||
|
||||
def build_email_update_description(bounced:, new_email:, previous_org_email:, previous_contact_email:, reason: nil)
|
||||
lines = []
|
||||
lines << "Bounce / email non recapitabile: #{bounced}" if bounced.present?
|
||||
lines << "Motivo: #{reason}" if reason.present?
|
||||
lines << "Email organizzazione precedente: #{previous_org_email}" if previous_org_email.present?
|
||||
lines << "Email contatto precedente: #{previous_contact_email}" if previous_contact_email.present?
|
||||
if new_email.present?
|
||||
lines << "Nuova email impostata: #{new_email}"
|
||||
elsif bounced.present?
|
||||
lines << "Nessuna email alternativa trovata; indirizzo marcato come non valido."
|
||||
end
|
||||
lines.join("\n")
|
||||
end
|
||||
|
||||
|
||||
def with_project(code)
|
||||
project = Project.active.find_by(code: code.to_s)
|
||||
return err("Progetto non trovato", status: :not_found) if project.nil?
|
||||
@@ -230,6 +428,8 @@ module Crm
|
||||
region: org.region,
|
||||
country: org.country,
|
||||
email: org.email,
|
||||
email_invalid: org.email_invalid,
|
||||
bounced_email: org.bounced_email,
|
||||
phone: org.phone,
|
||||
website: org.website,
|
||||
lead_source: org.lead_source,
|
||||
@@ -259,6 +459,8 @@ module Crm
|
||||
full_name: contact.full_name,
|
||||
role: contact.role,
|
||||
email: contact.email,
|
||||
email_invalid: contact.email_invalid,
|
||||
bounced_email: contact.bounced_email,
|
||||
phone: contact.phone,
|
||||
mobile: contact.mobile,
|
||||
primary_contact: contact.primary_contact,
|
||||
|
||||
Reference in New Issue
Block a user