extract email assets
This commit is contained in:
@@ -16,7 +16,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
||||||
|
|
||||||
@body = @email_message&.body.presence ||
|
@body = @email_message&.normalized_body.presence ||
|
||||||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
||||||
@submitter.template&.preferences&.dig('request_email_body').presence
|
@submitter.template&.preferences&.dig('request_email_body').presence
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :account_configs, dependent: :destroy
|
has_many :account_configs, dependent: :destroy
|
||||||
has_many :email_messages, dependent: :destroy
|
has_many :email_messages, dependent: :destroy
|
||||||
|
has_many :email_message_assets, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
has_many :template_folders, dependent: :destroy
|
has_many :template_folders, dependent: :destroy
|
||||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ class EmailMessage < ApplicationRecord
|
|||||||
|
|
||||||
before_validation :set_sha1, on: :create
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def normalized_body
|
||||||
|
@normalized_body ||=
|
||||||
|
if body&.include?(EmailMessages::ASSET_PREFIX)
|
||||||
|
EmailMessages.rebuild_body_with_assets(account_id, body)
|
||||||
|
else
|
||||||
|
body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_sha1
|
def set_sha1
|
||||||
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_message_assets
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_message_assets_on_account_id_and_sha1 (account_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
#
|
||||||
|
class EmailMessageAsset < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest(data.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :message, t('body'), class: 'label' %>
|
<%= f.label :message, t('body'), class: 'label' %>
|
||||||
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||||
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
||||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :message, t('body'), class: 'label' %>
|
<%= ff.label :message, t('body'), class: 'label' %>
|
||||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateEmailMessageAssets < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :email_message_assets do |t|
|
||||||
|
t.references :account, null: false, foreign_key: true, index: false
|
||||||
|
t.text :data, null: false
|
||||||
|
t.string :sha1, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :email_message_assets, %i[account_id sha1], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
+11
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
|
ActiveRecord::Schema[8.1].define(version: 2026_06_28_120000) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
@@ -214,6 +214,15 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
|
|||||||
t.index ["message_id"], name: "index_email_events_on_message_id"
|
t.index ["message_id"], name: "index_email_events_on_message_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "email_message_assets", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.text "data", null: false
|
||||||
|
t.string "sha1", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id", "sha1"], name: "index_email_message_assets_on_account_id_and_sha1", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "email_messages", force: :cascade do |t|
|
create_table "email_messages", force: :cascade do |t|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "author_id", null: false
|
t.bigint "author_id", null: false
|
||||||
@@ -579,6 +588,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
|
|||||||
add_foreign_key "dynamic_document_versions", "dynamic_documents"
|
add_foreign_key "dynamic_document_versions", "dynamic_documents"
|
||||||
add_foreign_key "dynamic_documents", "templates"
|
add_foreign_key "dynamic_documents", "templates"
|
||||||
add_foreign_key "email_events", "accounts"
|
add_foreign_key "email_events", "accounts"
|
||||||
|
add_foreign_key "email_message_assets", "accounts"
|
||||||
add_foreign_key "email_messages", "accounts"
|
add_foreign_key "email_messages", "accounts"
|
||||||
add_foreign_key "email_messages", "users", column: "author_id"
|
add_foreign_key "email_messages", "users", column: "author_id"
|
||||||
add_foreign_key "encrypted_configs", "accounts"
|
add_foreign_key "encrypted_configs", "accounts"
|
||||||
|
|||||||
+59
-2
@@ -1,13 +1,70 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module EmailMessages
|
module EmailMessages
|
||||||
|
MIN_BODY_SIZE = 2.kilobytes
|
||||||
|
MIN_ASSET_SIZE = 256.bytes
|
||||||
|
STYLE_REGEXP = %r{<style[^>]*>.*?</style>(?:\s*<style[^>]*>.*?</style>)*}mi
|
||||||
|
BASE64_REGEXP = %r{(data:[^,]*;base64,)([A-Za-z0-9+/=]+)}
|
||||||
|
ASSET_REGEXP = Regexp.union(STYLE_REGEXP, BASE64_REGEXP)
|
||||||
|
ASSET_PREFIX = '[[asset:'
|
||||||
|
PLACEHOLDER_REGEXP = /\[\[asset:(\h{40})\]\]/
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def find_or_create_for_account_user(account, user, subject, body)
|
def find_or_create_for_account_user(account, user, subject, body)
|
||||||
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
|
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
|
||||||
|
|
||||||
message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
body, assets = maybe_extract_assets(account, body)
|
||||||
|
|
||||||
account.email_messages.find_by(sha1: message.sha1) || message.tap { |m| m.save!(validate: false) }
|
new_message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
||||||
|
|
||||||
|
message = account.email_messages.find_by(sha1: new_message.sha1)
|
||||||
|
|
||||||
|
message ||= new_message.tap do |m|
|
||||||
|
m.save!(validate: false)
|
||||||
|
|
||||||
|
save_new_assets!(account, assets)
|
||||||
|
end
|
||||||
|
|
||||||
|
message
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_new_assets!(account, assets)
|
||||||
|
return if assets.blank?
|
||||||
|
|
||||||
|
existing_assets_sha1 = account.email_message_assets.where(sha1: assets.map(&:sha1)).pluck(:sha1)
|
||||||
|
|
||||||
|
assets.each do |asset|
|
||||||
|
asset.save!(validate: false) if existing_assets_sha1.exclude?(asset.sha1)
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_extract_assets(account, body)
|
||||||
|
return [body, []] if body.blank? || body.bytesize < MIN_BODY_SIZE
|
||||||
|
|
||||||
|
assets_index = {}
|
||||||
|
|
||||||
|
result = body.gsub(ASSET_REGEXP) do
|
||||||
|
match = Regexp.last_match
|
||||||
|
prefix, data = match[1] ? [match[1], match[2]] : ['', match[0]]
|
||||||
|
|
||||||
|
next match[0] if data.blank? || data.bytesize < MIN_ASSET_SIZE
|
||||||
|
|
||||||
|
asset = account.email_message_assets.new(data:).tap(&:validate)
|
||||||
|
assets_index[asset.sha1] = asset
|
||||||
|
|
||||||
|
"#{prefix}#{ASSET_PREFIX}#{asset.sha1}]]"
|
||||||
|
end
|
||||||
|
|
||||||
|
[result, assets_index.values]
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_body_with_assets(account_id, body)
|
||||||
|
shas = body.scan(PLACEHOLDER_REGEXP).flatten.uniq
|
||||||
|
data = EmailMessageAsset.where(account_id:, sha1: shas).pluck(:sha1, :data).to_h
|
||||||
|
|
||||||
|
body.gsub(PLACEHOLDER_REGEXP) { data[Regexp.last_match(1)] || Regexp.last_match(0) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user