Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled

Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 23:01:48 +02:00
co-authored by Cursor
commit c4e5f289cf
258 changed files with 11293 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
class Mailings::InlineImages
BLOB_PATH = %r{/rails/active_storage/blobs/(?:redirect/|proxy/)?([^/?#]+)}
def self.call(mailer, html)
new(mailer).call(html)
end
def initialize(mailer)
@mailer = mailer
end
def call(html)
fragment = Nokogiri::HTML::DocumentFragment.parse(html.to_s)
fragment.css("img").each_with_index do |img, index|
blob = blob_from(img["src"])
next unless blob
name = "inline-#{index}-#{blob.filename}"
@mailer.attachments.inline[name] = {
mime_type: blob.content_type,
content: blob.download
}
img["src"] = @mailer.attachments[name].url
width = img["width"].presence
styles = ["max-width: 100%", "height: auto"]
styles << "width: #{width}px" if width.present?
img["style"] = [img["style"], *styles].compact.join("; ")
end
fragment.css("figure").each do |figure|
image = figure.at_css("img")
image ? figure.replace(image) : figure.remove
end
fragment.to_html
end
private
def blob_from(src)
signed_id = src.to_s[BLOB_PATH, 1]
return if signed_id.blank?
ActiveStorage::Blob.find_signed(signed_id)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveRecord::RecordNotFound
nil
end
end
@@ -0,0 +1,67 @@
class Mailings::RecipientBuilder
def initialize(mailing)
@mailing = mailing
end
def call
split_index = 0
ordered_scope.each do |org|
contact = org.primary_contact || org.contacts.min_by(&:id)
email = contact&.email.presence || org.email.presence
attrs = {
organization: org,
contact: contact,
email: email&.downcase
}
if email.blank?
attrs[:status] = "skipped"
attrs[:skip_reason] = "manca email"
elsif !email.match?(URI::MailTo::EMAIL_REGEXP)
attrs[:status] = "skipped"
attrs[:skip_reason] = "email non valida"
else
attrs[:status] = "pending"
end
assign_variant!(attrs, org, split_index)
split_index += 1 if attrs[:status] == "pending"
@mailing.mailing_recipients.create!(attrs)
end
@mailing.mailing_recipients.ordered
end
private
def ordered_scope
scope.reorder(Arel.sql("organizations.list_position ASC NULLS LAST"), "organizations.name ASC")
end
def scope
orgs = Organization.for_project(@mailing.project).includes(:contacts, :opportunities)
org_ids =
case @mailing.audience
when "to_send"
Opportunity.where(project_id: @mailing.project_id, send_status: "to_send").select(:organization_id)
when "test_a"
Opportunity.where(project_id: @mailing.project_id, ab_variant: "A").select(:organization_id)
when "test_b"
Opportunity.where(project_id: @mailing.project_id, ab_variant: "B").select(:organization_id)
else
orgs.select(:id)
end
orgs.where(id: org_ids)
end
def assign_variant!(attrs, org, split_index)
return unless @mailing.ab_test?
opportunity = org.campaign_opportunity(@mailing.project)
recorded = opportunity&.ab_variant.to_s.upcase
variant =
if @mailing.ab_from_record? && recorded.in?(%w[A B])
recorded
else
split_index.even? ? "A" : "B"
end
attrs[:ab_variant] = variant
end
end