Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions app/controllers/mailbox/email_templates_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Mailbox::EmailTemplatesController < Mailbox::BaseController

def index

@email_templates = EmailTemplate.all
end

def new
Expand All @@ -13,17 +13,55 @@ def show
def create
html_doc = params[:htmldoc]
re_doc = params[:redoc]
name = params[:name]

email_template = EmailTemplate.create!(html: html_doc, template: re_doc, name: name)


render json: { id: 1 }
render json: { id: email_template.id }
end

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

def update
email_template = EmailTemplate.find(params[:id])
html_doc = params[:htmldoc]
re_doc = params[:redoc]
name = params[:name]
email_template.update!(html: html_doc, template: re_doc, name: name)

render json: { id: email_template.id }
end

def destroy
def test_send
email_template = EmailTemplate.find(params[:id])
dynamic_segments = email_template.dynamic_segments
symbol_mapping = ActiveSupport::HashWithIndifferentAccess.new
dynamic_segments.each do |segment|
symbol_mapping[segment] = params[segment]
end

email_body = email_template.inject_dynamic_segments(symbol_mapping)
from_address = "#{Apartment::Tenant.current}@#{ENV['APP_HOST']}"
email_subject = "#{email_template.name} test email"
email_thread = MessageThread.create!(recipients: [current_user.email], subject: email_subject)
email_body.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
email_body.encode!('UTF-8', 'UTF-16')
email_message = email_thread.messages.create!(
content: email_body,
from: from_address
)
EMailer.with(message: email_message, message_thread: email_thread).ship.deliver_later

flash.notice = "Template test email sent to #{current_user.email}"
redirect_to edit_mailbox_email_template_path(email_template.id)
end

def destroy
email_template = EmailTemplate.find(params[:id])
flash.notice = "#{email_template.name} destroyed!"
email_template.destroy!
redirect_to mailbox_email_templates_path
end
end
36 changes: 36 additions & 0 deletions app/helpers/messages_helper.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
module MessagesHelper
# Extract the visible email content from HTML, removing meta tags, style tags, etc.
# This is used to display email content in the message thread view and email sending
def render_email_content(message)
return '' if message.content.blank?

# Use to_s to preserve ActionText attachments
html_content = message.content.to_s

# Parse the HTML
doc = Nokogiri::HTML.fragment(html_content)

# Remove the trix-content wrapper div if present (do this first)
trix_div = doc.at_css('div.trix-content')
if trix_div
# Replace the trix-content div with its children
trix_div.replace(trix_div.children.to_html)
# Re-parse after removing trix wrapper
doc = Nokogiri::HTML.fragment(doc.to_html)
end

# Remove meta tags, style tags, title, and link tags
doc.css('meta').remove
doc.css('style').remove
doc.css('title').remove
doc.css('link').remove

# Get the HTML string
result = doc.to_html

# Remove leading text nodes that appear before the first HTML tag
# This handles orphaned text like "My Email" from the title tag
# Match any text at the beginning that comes before the first < character
result = result.sub(/\A([^<]*?)(<)/, '\2')

result.html_safe
end
end
2 changes: 2 additions & 0 deletions app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class ApplicationMailer < ActionMailer::Base
helper MessagesHelper

default from: "#{Apartment::Tenant.current}@#{ENV['APP_HOST']}"
default "Message-ID" => lambda {"#{Digest::SHA2.hexdigest(Time.now.to_i.to_s)}@#{ENV['APP_HOST']}"}
end
45 changes: 45 additions & 0 deletions app/models/email_template.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class EmailTemplate < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
validates :name, presence: true

SYMBOL_CAPTURE_PATTERN = /(?<=\{\{)[a-z]+(?:\.[a-z]+)*(?=\}\})/

def dynamic_segments
html_doc = Base64.decode64(self.html)
html_doc.scan(SYMBOL_CAPTURE_PATTERN)
end

def inject_dynamic_segments(properties_obj = {})
# pass a hash of properties or a API Resource instance and get dynamic HTML
# usage option 1: EmailTemplate.last.inject_dynamic_segments({name: 'hideo kojima', link: "https://mgs.snake"})
# usage option 2: EmailTemplate.last.inject_dynamic_segments(ApiResource.last)
# works by extracting any variable defined between: {{}}
# example of including a variable called 'name': {{name}}

html_doc = Base64.decode64(self.html)
symbols = self.dynamic_segments
symbol_mapping = ActiveSupport::HashWithIndifferentAccess.new
output = html_doc

if properties_obj.class == ApiResource
api_resource = properties_obj
symbols.each do |symbol|
symbol_mapping[symbol] = api_resource.properties[symbol]
end
elsif properties_obj.class == Hash || properties_obj.class == ActiveSupport::HashWithIndifferentAccess
symbols.each do |symbol|
symbol_mapping[symbol] = properties_obj.with_indifferent_access[symbol]
end
else
raise "dynamic segment mapping for Email Template is unrecognized. Please pass a hash of properties or API Resource instance"
end

symbols.each do |symbol|
mapped_value = symbol_mapping[symbol]
raise "dynamic segment token has no value, please ensure a value is provided" if mapped_value.nil? || mapped_value.empty?
output = output.gsub("{{#{symbol}}}", mapped_value)
end
return output
end
end
2 changes: 1 addition & 1 deletion app/views/e_mailer/ship.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<% tracking_link = "#{root_url(subdomain: @subdomain.subdomain_name)}email_tracking/open/?emails=#{@recipients.join(',')}&message_id=#{@message.id}&email_uuid=#{@message.email_message_id}" %>
<img src='<%= tracking_link %>' width="1" height="1">
<% end %>
<%= @message.content %>
<%= render_email_content(@message).html_safe %>
<% if @subdomain.email_signature.present? %>
<div style="margin-top: 20px">
<%= @subdomain.email_signature %>
Expand Down
78 changes: 78 additions & 0 deletions app/views/mailbox/email_templates/edit.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.page-header
.h2
= link_to 'Email Templates', mailbox_email_templates_path
.container.my-2
= label_tag "name"
= text_field_tag "name", @email_template.name, id: 'email_template_name'
.container.my-2
= label_tag "Slug: "
= @email_template.slug
.container.my-2
= label_tag "Dynamic segments ( defined as {{}} ): "
= @email_template.dynamic_segments.join(', ')
.container.my-2
= label_tag "Test send: "
= form_tag(test_send_mailbox_email_template_path(@email_template.id), method: "POST", class: 'form-group') do
- @email_template.dynamic_segments.each do |segment|
= label_tag(segment.to_sym, "#{segment}:")
= text_field_tag(segment.to_sym, nil, class: 'form-control', required: true)
= submit_tag("Email me this template!", class: 'btn btn-secondary my-2')
.container.my-2
= label_tag "Copy HTML: "
= button_tag "Copy to clipboard", id: 'copyRevolvHtml'

.container
#VioletEmailEditor

%button#VioletEmailEditorSubmit.btn.btn-primary.my-2
= "Save"
= link_to "Delete", mailbox_email_template_path(@email_template.id), method: :delete, class: 'btn btn-danger', data: { confirm: 'Are you sure?' }

:javascript
let template = "#{escape_javascript(@email_template.template)}"
let id = "#{@email_template.id}"
const app = Revolvapp('#VioletEmailEditor', {
editor: {
path: '/revolvapp-2-3-10/',
},
content: template
});

$('#copyRevolvHtml').click(function() {
let htmldoc = app.editor.getHtml()

navigator.clipboard.writeText(htmldoc);

// Alert the copied text
alert("Copied html");
});

$('#VioletEmailEditorSubmit').click(function() {
$("#VioletEmailEditorSubmit").prop("disabled", true)
let authenticityToken = $('meta[name="csrf-token"]').attr('content');

let htmldoc = app.editor.getHtml()
let redoc = app.editor.getTemplate(true)
let name = $('#email_template_name').val()
const endpoint = "#{mailbox_email_template_path}"

if (name && htmldoc) {
$.ajax({
url: endpoint,
type: 'PATCH',
headers: {
'X-CSRF-Token': authenticityToken
},
data: JSON.stringify({ htmldoc: btoa(htmldoc), redoc: redoc, name: name, id: id }),
contentType: "application/json; charset=utf-8",
success: function(response) {
window.location.href = `${endpoint}/${response.id}/edit`
window.location.reload()
}
});
} else {
window.alert('Please give your template a name and set its content')
}

$("#VioletEmailEditorSubmit").prop("disabled", false)
});
9 changes: 9 additions & 0 deletions app/views/mailbox/email_templates/index.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.page-header
= link_to 'New Email Template', new_mailbox_email_template_path, class: 'btn btn-secondary float-right mx-1'
%h2
= link_to "Email", mailbox_path
Templates
%ol
- @email_templates.each do |email_template|
%li
= link_to email_template.name, edit_mailbox_email_template_path(email_template.id)
42 changes: 28 additions & 14 deletions app/views/mailbox/email_templates/new.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
.page-header
.h2
= 'Email Templates'
.container.m-2
.h3
= "Docs:"
%a{href: "https://imperavi.com/legacy/revolvapp/docs/syntax/tags/", target: '_blank'}
Imperavi Revolvapp
.container.m-2
.h3
= "AI prompt:"
Expand All @@ -10,10 +15,14 @@
= "... copy pasted editor code"
.p
= "using the above markup language create me a..."
.container.my-2
= label_tag "name"
= text_field_tag "name", nil, id: 'email_template_name'

.container
#VioletEmailEditor

%button#VioletEmailEditorSubmit
%button#VioletEmailEditorSubmit.btn.btn-primary.my-2
= "Save"

:javascript
Expand All @@ -23,26 +32,31 @@
template: '/revolvapp-2-3-10/templates/index.html',
}
});
app.start()

$('#VioletEmailEditorSubmit').click(function() {
$("#VioletEmailEditorSubmit").prop("disabled", true)
let authenticityToken = $('meta[name="csrf-token"]').attr('content');

let htmldoc = app.editor.getHtml()
let redoc = app.editor.getTemplate(true)
let name = $('#email_template_name').val()
const endpoint = "#{mailbox_email_templates_path}"
if (name && htmldoc) {
$.ajax({
url: endpoint,
type: 'POST',
headers: {
'X-CSRF-Token': authenticityToken
},
data: JSON.stringify({ htmldoc: btoa(htmldoc), redoc: redoc, name: name }),
contentType: "application/json; charset=utf-8",
success: function(response) {
window.location.href = `${endpoint}/${response.id}/edit`
}
});
} else {
window.alert('Please give your template a name and set its content')
}

$.ajax({
url: endpoint,
type: 'POST',
headers: {
'X-CSRF-Token': authenticityToken
},
data: JSON.stringify({ htmldoc: htmldoc, redoc: redoc }),
contentType: "application/json; charset=utf-8",
success: function(response) {
window.location.href = `${endpoint}/${response.id}`
}
});
$("#VioletEmailEditorSubmit").prop("disabled", false)
});
2 changes: 1 addition & 1 deletion app/views/mailbox/mailbox/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

.page-header
= link_to I18n.t('views.mailbox.index.header.actions.new'), new_mailbox_message_thread_path, class: 'btn btn-success float-right mx-1'
= link_to 'Email Templates', new_mailbox_email_template_path, class: 'btn btn-secondary float-right mx-1'
= link_to 'Email Templates', mailbox_email_templates_path, class: 'btn btn-secondary float-right mx-1'
.h2
= I18n.t('views.mailbox.index.header.title')
= Subdomain.current.mailing_address
Expand Down
6 changes: 4 additions & 2 deletions app/views/mailbox/message_threads/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
= render partial: 'messages/form', locals: { f: f, render_submit: true }
- @message_thread.messages.each do |message|
.card.my-3
.card-body
.card-body
.card-subtitle.mb-2.text-muted
= message.from
- if !message.from && Subdomain.current.track_email_opens
.card-subtitle.mb-2.text-muted
= message.opened ? 'opened' : 'not opened yet'
.card-text.bg-light.px-2.py-3
= message.content
= render_email_content(message)
- if message.attachments.any?
.card-text.bg-dark.px-2.py-3.text-white
Attachments
%ul{class: 'list-group'}
- message.attachments.each do |attachment|
= link_to attachment.filename, rails_blob_path(attachment, disposition: 'attachment')


25 changes: 25 additions & 0 deletions config/initializers/action_text.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Configure ActionText to allow style tags and attributes
# This is necessary for email templates (e.g., Revolvapp) that include CSS styles and tables
Rails.application.config.to_prepare do
# Allow the style attribute on all tags
ActionText::ContentHelper.allowed_attributes.add('style')
ActionText::ContentHelper.allowed_attributes.add('cellpadding')
ActionText::ContentHelper.allowed_attributes.add('cellspacing')
ActionText::ContentHelper.allowed_attributes.add('border')
ActionText::ContentHelper.allowed_attributes.add('width')
ActionText::ContentHelper.allowed_attributes.add('align')
ActionText::ContentHelper.allowed_attributes.add('valign')
ActionText::ContentHelper.allowed_attributes.add('bgcolor')

# Allow the style tag itself
ActionText::ContentHelper.allowed_tags.add('style')

# Allow table-related tags for email templates
ActionText::ContentHelper.allowed_tags.add('table')
ActionText::ContentHelper.allowed_tags.add('tbody')
ActionText::ContentHelper.allowed_tags.add('thead')
ActionText::ContentHelper.allowed_tags.add('tfoot')
ActionText::ContentHelper.allowed_tags.add('tr')
ActionText::ContentHelper.allowed_tags.add('td')
ActionText::ContentHelper.allowed_tags.add('th')
end
Loading