Class: EmailTemplatesController

Inherits:
CrmController show all
Includes:
Controllers::Destroyable
Defined in:
app/controllers/email_templates_controller.rb

Overview

== Schema Information

Table name: email_templates

id :integer not null, primary key
from :string(255)
to :string(255)
cc :string(255)
bcc :string(255)
resource_type :string(255)
resource_id :integer
body :text
subject :string(255)
creator_id :integer
updater_id :integer
description :string(255)
state :string(255)
css :text
stylesheet :string(255)
created_at :datetime not null
updated_at :datetime not null
system_code :string(20)
category :string
template :string
preview_text :string

Constant Summary

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Constants included from Www::SeoHelper

Www::SeoHelper::AWARDS, Www::SeoHelper::CA_ADDRESS, Www::SeoHelper::CA_BUSINESS_HOURS, Www::SeoHelper::CA_CONTACT_POINT, Www::SeoHelper::CA_CURRENCIES, Www::SeoHelper::CA_DESCRIPTION, Www::SeoHelper::CA_FOUNDING_DATE, Www::SeoHelper::CA_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::CA_LEGAL_NAME, Www::SeoHelper::CA_LOCAL_BUSINESS, Www::SeoHelper::CA_ONLINE_STORE, Www::SeoHelper::CA_RETURN_POLICY, Www::SeoHelper::CA_SALES_DEPARTMENT, Www::SeoHelper::CA_SERVICE_AREA, Www::SeoHelper::CA_URL, Www::SeoHelper::CA_VAT_ID, Www::SeoHelper::CA_WAREHOUSE_DEPARTMENT, Www::SeoHelper::CA_WAREHOUSE_HOURS, Www::SeoHelper::COMPANY_EMAIL, Www::SeoHelper::COMPANY_LOGO, Www::SeoHelper::COMPANY_NAME, Www::SeoHelper::COMPANY_SLOGAN, Www::SeoHelper::EXPERTISE, Www::SeoHelper::FAX_NUMBER, Www::SeoHelper::GS1_COMPANY_PREFIX, Www::SeoHelper::ISO6523_CODE, Www::SeoHelper::PAYMENT_METHODS, Www::SeoHelper::PHONE_NUMBER, Www::SeoHelper::PRIMARY_NAICS, Www::SeoHelper::REFUND_TYPE, Www::SeoHelper::RETURN_FEES, Www::SeoHelper::RETURN_METHOD, Www::SeoHelper::RETURN_POLICY_CATEGORY, Www::SeoHelper::SECONDARY_NAICS, Www::SeoHelper::SOCIAL_PROFILES, Www::SeoHelper::US_ADDRESS, Www::SeoHelper::US_BUSINESS_HOURS, Www::SeoHelper::US_CONTACT_POINT, Www::SeoHelper::US_CURRENCIES, Www::SeoHelper::US_DESCRIPTION, Www::SeoHelper::US_FOUNDING_DATE, Www::SeoHelper::US_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::US_IMAGE, Www::SeoHelper::US_LEGAL_NAME, Www::SeoHelper::US_LOCAL_BUSINESS, Www::SeoHelper::US_ONLINE_STORE, Www::SeoHelper::US_RETURN_POLICY, Www::SeoHelper::US_SALES_DEPARTMENT, Www::SeoHelper::US_SERVICE_AREA, Www::SeoHelper::US_TAX_ID, Www::SeoHelper::US_URL, Www::SeoHelper::US_WAREHOUSE_DEPARTMENT, Www::SeoHelper::US_WAREHOUSE_HOURS

Constants included from IconHelper

IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY

Instance Method Summary collapse

Methods included from Controllers::Destroyable

#destroy, #perform_destroy

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #initialize_crm_lazy_chunks, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #append_token, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #stamp_impersonation_context, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #authenticate_account_from_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from ApplicationHelper

#better_number_to_currency, #check_force_logout, #check_or_cross, #check_or_times, #error_messages, #general_disclaimer_on_product_installation_and_local_codes, #gridjs_from_html_table, #gridjs_table, #is_wy_ip, #line_break, #parent_layout, #pass_or_fail, #render_error_messages_list, #render_video_card, #resolved_auth_form_turbo_frame, #return_path_or, #safe_css_color, #set_return_path_if_present, #set_section_if_present, #tab_frame_id, #to_underscore, #track_page?, #turbo_section_wrapper, #turbo_tabs_request?, #url_on_same_domain_as_request, #widget_index_daily_focus_index_path, #working_hours?, #yes_or_no, #yes_or_no_highlighted, #yes_or_no_with_check_or_cross, #youtube_video

Methods included from UppyUploaderHelper

#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader

Methods included from Www::ImagesHelper

#image_asset_tag, #image_asset_url

Methods included from Www::SeoHelper

#add_page_schema, #canada?, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Methods included from IconHelper

#account_nav_icon, #fa_icon, #star_rating_html

Instance Method Details

#bodyObject



172
173
174
175
176
177
178
179
# File 'app/controllers/email_templates_controller.rb', line 172

def body
  @email_template = EmailTemplate.find(params[:id])
  authorize! :read, EmailTemplate
  @body = @email_template.body
  @preview_mode = true
  @title = @email_template.subject
  render template: '/communication_mailer/email', layout: false
end

#browser_previewObject

Render the email template directly in the browser (no iframe)
Used for previewing how the email will look in email clients



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'app/controllers/email_templates_controller.rb', line 248

def browser_preview
  @email_template = EmailTemplate.find(params[:id])

  # Build merge options for rendering
  cb = CommunicationBuilder.new(
    sender_party_id: params[:sender_party_id] || @context_user&.id,
    recipient_party_id: params[:recipient_party_id],
    resource_id: params[:resource_id],
    resource_type: params[:resource_type],
    email_template: @email_template,
    merge_options: { ignore_errors: true }
  )

  # Determine which version to render (v4 if available and ready, otherwise v3)
  version = params[:version] || (@email_template.redactor_4_ready? ? 'v4' : 'v3')

  if version == 'v4' && @email_template.body_v4_email.present?
    html = @email_template.render_body_v4(cb.merge_options)
  else
    html = @email_template.render_body(cb.merge_options)
  end

  render html: html.html_safe, layout: false
end

#copyObject



158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'app/controllers/email_templates_controller.rb', line 158

def copy
  authorize! :create, EmailTemplate
  @email_template = EmailTemplate.find(params[:id])

  # Block copying of Redactor 3 templates to prevent legacy content from spreading
  unless @email_template.redactor_4_ready?
    redirect_to @email_template, alert: 'Cannot copy legacy Redactor 3 templates. Please migrate this template to Redactor 4 first.'
    return
  end

  @email_template = @email_template.deep_dup
  render action: :new
end

#createObject



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'app/controllers/email_templates_controller.rb', line 118

def create
  authorize! :create, EmailTemplate
  @email_template = EmailTemplate.new(params[:email_template])
  if @email_template.save
    if params[:commit] == 'Save and send preview'
      redirect_to send_preview_email_template_path(@email_template), info: 'Email Template was successfully created.'
    else
      redirect_to @email_template, info: 'Email Template was successfully created.'
    end
  else
    render action: :new, status: :unprocessable_entity
  end
end

#do_send_previewObject



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/controllers/email_templates_controller.rb', line 273

def do_send_preview
  @email_template = EmailTemplate.find(params[:id])
  @source_id = params[:source_id]
  multi_emails = params.dig(:email, :email_address)
  emails = multi_emails.split(/[,;\s]+/).map(&:squish).map(&:presence).compact
  # Determine if we should force v4 rendering
  force_v4 = params[:version] == 'v4'

  # Process "Preview with Real Data" parameters
  sender_party_id = params[:sender_party_id].presence || @context_user.id
  recipient_party_id = nil
  resource_type = nil
  resource_id = nil

  # Look up recipient party from customer email
  if params[:recipient].present?
    contact_point = ContactPoint.where(detail: params[:recipient]).where.not(party_id: nil).first
    recipient_party_id = contact_point&.party_id
  end

  if (result = resolve_resource_from_params)
    found, resource_type, fallback_party_id = result
    resource_id = found.id
    recipient_party_id ||= fallback_party_id
  end

  communications = []
  failed_emails = []
  emails.each do |email|
    comm = CommunicationBuilder.new(
      sender_party_id: sender_party_id,
      recipient_party_id: recipient_party_id,
      email_template_id: @email_template.id,
      source_id: @source_id,
      resource_type: resource_type,
      resource_id: resource_id,
      emails: email,
      recipient_party: ContactPoint.where(detail: email).where.not(party_id: nil).first.try(:party),
      force_v4: force_v4
    ).create(send_immediately: true, skip_suppression_check: true)
    if comm.persisted?
      communications << comm
    else
      failed_emails << { email: email, errors: comm.errors.full_messages.join(', ') }
    end
  end

  messages = []
  if communications.any?
    links = communications.map { |c| view_context.link_to("Communication id: #{c.id} [#{c.state}]", communication_path(c.id)) }.join(', ')
    messages << "Preview email has been sent. #{links}".html_safe
  end
  if failed_emails.any?
    failed_msgs = failed_emails.map { |f| "#{f[:email]}: #{f[:errors]}" }.join('; ')
    messages << "Failed to send preview to: #{failed_msgs}"
  end

  if failed_emails.any? && communications.empty?
    flash[:error] = messages.join(' ')
  elsif failed_emails.any?
    flash[:warning] = messages.join(' ')
  else
    flash[:info] = messages.join(' ')
  end

  redirect_to_return_path_or_default(email_template_path(@email_template))
end

#editObject



113
114
115
116
# File 'app/controllers/email_templates_controller.rb', line 113

def edit
  @email_template = EmailTemplate.find(params[:id])
  authorize! :update, EmailTemplate
end

#exportObject



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/controllers/email_templates_controller.rb', line 68

def export
  authorize! :export, EmailTemplate
  require 'tempfile'
  begin
    file = Tempfile.new('foo')
    email_templates = EmailTemplate.all
    email_templates = email_templates.where(id: params[:email_template_ids]) if params[:email_template_ids].present?
    # pass the file handle as the second parameter to dump
    file.write(email_templates.to_yaml)
    file.flush
    file.fsync
    send_file_accelerated(file.path, download: true, file_name: "email_template_export_#{Time.current.to_i}.yaml")
  ensure
    file.close
  end
end

#importObject



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'app/controllers/email_templates_controller.rb', line 85

def import
  authorize! :import, EmailTemplate
  uploaded_io = params[:file]
  email_templates = YAML.load(uploaded_io.read)
  res = []
  email_templates.each do |et|
    email_template = EmailTemplate.where('description ILIKE ?', et.description).first
    if email_template
      email_template.update(et.attributes)
    else
      email_template = EmailTemplate.create(et.attributes)
    end

    if email_template.errors.empty?
      res << email_template.id
    else
      flash[:error] ||= ''
      flash[:error] << "Email Template #{et.id} could not be saved: #{email_template.errors_to_s}. "
    end
  end
  redirect_to email_templates_path, info: "Email Templates #{res.join} were successfully imported"
end

#improve_styleObject

POST /email_templates/:id/improve_style
Improves styling of existing Redactor 4 content using AI without changing content



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'app/controllers/email_templates_controller.rb', line 214

def improve_style
  @email_template = EmailTemplate.find(params[:id])
  authorize! :update, @email_template

  body_v4 = params[:body_v4]

  migrator = EmailTemplate::ContentMigrator.new
  result = migrator.improve_style(body_v4:)

  if result[:success]
    render json: {
      success: true,
      body_v4: result[:body_v4]
    }
  else
    render json: {
      success: false,
      error: result[:error] || 'Unknown error during style improvement'
    }, status: :unprocessable_entity
  end
rescue StandardError => e
  render json: {
    success: false,
    error: "Style improvement failed: #{e.message}"
  }, status: :internal_server_error
end

#inactiveObject



147
148
149
150
151
152
153
154
155
156
# File 'app/controllers/email_templates_controller.rb', line 147

def inactive
  @email_template = EmailTemplate.find(params[:id])
  authorize! :update, EmailTemplate
  if @email_template.update(state: 'archived')
    flash[:info] = 'Email Template was successfully updated.'
    redirect_to_return_path_or_default email_template_path(@email_template)
  else
    render action: :edit, status: :unprocessable_entity
  end
end

#indexObject



31
32
33
34
35
36
37
38
39
# File 'app/controllers/email_templates_controller.rb', line 31

def index
  @q = ViewEmailTemplate.includes(:resource)
  @q = @q.where("(resource_type = 'Employee' and resource_id = ?) or resource_type = 'Company' or resource_type IS NULL", @context_user.id) if cannot?(:manage, EmailTemplate)
  @q = @q.where('referenced_in_campaign = true') if params[:referenced_in_campaign].to_b
  @q = @q.where('referenced_in_activity = true') if params[:referenced_in_activity].to_b
  @q = @q.ransack(params[:q])
  @q.sorts = 'created_at DESC' if @q.sorts.empty?
  @pagy, @email_templates = pagy(@q.result, limit: 100)
end

#migrate_contentObject

POST /email_templates/:id/migrate_content
Converts legacy Redactor 3 content to Redactor 4 format using AI



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/controllers/email_templates_controller.rb', line 183

def migrate_content
  @email_template = EmailTemplate.find(params[:id])
  authorize! :update, @email_template

  migrator = EmailTemplate::ContentMigrator.new
  result = migrator.process(
    body: @email_template.body,
    css: @email_template.css
  )

  if result[:success]
    render json: {
      success: true,
      body_v4: result[:body_v4],
      css_v4: result[:css_v4]
    }
  else
    render json: {
      success: false,
      error: result[:error] || 'Unknown error during conversion'
    }, status: :unprocessable_entity
  end
rescue StandardError => e
  render json: {
    success: false,
    error: "Conversion failed: #{e.message}"
  }, status: :internal_server_error
end

#newObject



108
109
110
111
# File 'app/controllers/email_templates_controller.rb', line 108

def new
  authorize! :create, EmailTemplate
  @email_template = EmailTemplate.new(belongs_to_resource: "Employee|#{current_user.id}", stylesheet: EmailTemplate::DEFAULT_STYLESHEET, template: EmailTemplate::DEFAULT_TEMPLATE)
end

#send_previewObject



241
242
243
244
# File 'app/controllers/email_templates_controller.rb', line 241

def send_preview
  @email_template = EmailTemplate.find(params[:id])
  @source_id = params[:source_id]
end

#showObject



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'app/controllers/email_templates_controller.rb', line 41

def show
  @email_template = EmailTemplate.find(params[:id])
  if params[:recipient].present?
    @recipient = params[:recipient].strip
    if @recipient.include?('@')
      cp = ContactPoint.where(detail: @recipient).where.not(party_id: nil).first
      @recipient_party_id = cp.party.id if cp
    end
  end
  if (result = resolve_resource_from_params)
    found, @resource_type, fallback_party_id = result
    @resource_id = found.id
    @recipient_party_id ||= fallback_party_id
  end
  @resource_type_param = params[:resource_type]
  @resource_ref = params[:resource_ref]
  @sender_party_id = params[:sender_party_id]

  @last_used_communication = @email_template.communications.order(created_at: :desc).select(:id, :created_at, :subject).first
  @usage_count_30d = @email_template.communications.where(created_at: 30.days.ago..).count

  respond_to do |format|
    format.html # show.html.erb
    format.json { render json: @email_template }
  end
end

#updateObject



132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'app/controllers/email_templates_controller.rb', line 132

def update
  @email_template = EmailTemplate.find(params[:id])
  authorize! :update, EmailTemplate
  if @email_template.update(params[:email_template])
    if params[:commit] == 'Save and send preview'
      redirect_to send_preview_email_template_path(@email_template), info: 'Email Template was successfully updated.'
    else
      flash[:info] = 'Email Template was successfully updated.'
      redirect_to_return_path_or_default email_template_path(@email_template)
    end
  else
    render action: :edit, status: :unprocessable_entity
  end
end