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, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #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::TurboSafeRedirect

#redirect_to

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, #render_unpermitted_parameters, #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_edge_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!, #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, #embedded_tab_frame_id, #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, #add_webpage_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



189
190
191
192
193
194
195
196
# File 'app/controllers/email_templates_controller.rb', line 189

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'app/controllers/email_templates_controller.rb', line 265

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')

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

  render html: html.html_safe, layout: false
end

#copyObject



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'app/controllers/email_templates_controller.rb', line 175

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



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

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_content
  end
end

#do_send_previewObject



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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'app/controllers/email_templates_controller.rb', line 290

def do_send_preview
  @email_template = EmailTemplate.find(params[:id])
  @source_id = params[:source_id]
  # Tom Select multi-input submits `email[email_address][]=...` (Array); the
  # legacy textarea submitted a single comma/semicolon-separated string. Accept
  # both and flatten down to one email per slot.
  multi_emails = params.dig(:email, :email_address)
  emails = Array.wrap(multi_emails)
                .flat_map { |v| v.to_s.split(/[,;\s]+/) }
                .map(&:squish)
                .filter_map(&:presence)
  # 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



130
131
132
133
# File 'app/controllers/email_templates_controller.rb', line 130

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

#exportObject



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

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



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'app/controllers/email_templates_controller.rb', line 102

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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/controllers/email_templates_controller.rb', line 231

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_content
  end
rescue StandardError => e
  render json: {
    success: false,
    error: "Style improvement failed: #{e.message}"
  }, status: :internal_server_error
end

#inactiveObject



164
165
166
167
168
169
170
171
172
173
# File 'app/controllers/email_templates_controller.rb', line 164

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_content
  end
end

#indexObject



48
49
50
51
52
53
54
55
56
# File 'app/controllers/email_templates_controller.rb', line 48

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



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/controllers/email_templates_controller.rb', line 200

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_content
  end
rescue StandardError => e
  render json: {
    success: false,
    error: "Conversion failed: #{e.message}"
  }, status: :internal_server_error
end

#newObject



125
126
127
128
# File 'app/controllers/email_templates_controller.rb', line 125

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



258
259
260
261
# File 'app/controllers/email_templates_controller.rb', line 258

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

#showObject



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/controllers/email_templates_controller.rb', line 58

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



149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'app/controllers/email_templates_controller.rb', line 149

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_content
  end
end