mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 02:02:27 +00:00
This PR adds sending custom HTML content in outgoing email messages
through Chatwoot's Email channels, while maintaining backward
compatibility with existing markdown rendering.
### API Usage
**Endpoint:** `POST
/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages`
```json
{
"content": "Fallback text content",
"email_html_content": "<div><h1>Welcome!</h1><p>This is <strong>custom HTML</strong></p></div>"
}
```
---------
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
236 lines
7.4 KiB
Ruby
236 lines
7.4 KiB
Ruby
class Messages::MessageBuilder
|
|
include ::FileTypeHelper
|
|
attr_reader :message
|
|
|
|
def initialize(user, conversation, params)
|
|
@params = params
|
|
@private = params[:private] || false
|
|
@conversation = conversation
|
|
@user = user
|
|
@account = conversation.account
|
|
@message_type = params[:message_type] || 'outgoing'
|
|
@attachments = params[:attachments]
|
|
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
|
return unless params.instance_of?(ActionController::Parameters)
|
|
|
|
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
|
@items = content_attributes&.dig(:items)
|
|
end
|
|
|
|
def perform
|
|
@message = @conversation.messages.build(message_params)
|
|
process_attachments
|
|
process_emails
|
|
# When the message has no quoted content, it will just be rendered as a regular message
|
|
# The frontend is equipped to handle this case
|
|
process_email_content if @account.feature_enabled?(:quoted_email_reply)
|
|
@message.save!
|
|
@message
|
|
end
|
|
|
|
private
|
|
|
|
# Extracts content attributes from the given params.
|
|
# - Converts ActionController::Parameters to a regular hash if needed.
|
|
# - Attempts to parse a JSON string if content is a string.
|
|
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
|
def content_attributes
|
|
params = convert_to_hash(@params)
|
|
content_attributes = params.fetch(:content_attributes, {})
|
|
|
|
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
|
return content_attributes if content_attributes.is_a?(Hash)
|
|
|
|
{}
|
|
end
|
|
|
|
# Converts the given object to a hash.
|
|
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
|
# Otherwise, returns the object as-is.
|
|
def convert_to_hash(obj)
|
|
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
|
|
|
obj
|
|
end
|
|
|
|
# Attempts to parse a string as JSON.
|
|
# If successful, returns the parsed hash with symbolized names.
|
|
# If unsuccessful, returns nil.
|
|
def parse_json(content)
|
|
JSON.parse(content, symbolize_names: true)
|
|
rescue JSON::ParserError
|
|
{}
|
|
end
|
|
|
|
def process_attachments
|
|
return if @attachments.blank?
|
|
|
|
@attachments.each do |uploaded_attachment|
|
|
attachment = @message.attachments.build(
|
|
account_id: @message.account_id,
|
|
file: uploaded_attachment
|
|
)
|
|
|
|
attachment.file_type = if uploaded_attachment.is_a?(String)
|
|
file_type_by_signed_id(
|
|
uploaded_attachment
|
|
)
|
|
else
|
|
file_type(uploaded_attachment&.content_type)
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_emails
|
|
return unless @conversation.inbox&.inbox_type == 'Email'
|
|
|
|
cc_emails = process_email_string(@params[:cc_emails])
|
|
bcc_emails = process_email_string(@params[:bcc_emails])
|
|
to_emails = process_email_string(@params[:to_emails])
|
|
|
|
all_email_addresses = cc_emails + bcc_emails + to_emails
|
|
validate_email_addresses(all_email_addresses)
|
|
|
|
@message.content_attributes[:cc_emails] = cc_emails
|
|
@message.content_attributes[:bcc_emails] = bcc_emails
|
|
@message.content_attributes[:to_emails] = to_emails
|
|
end
|
|
|
|
def process_email_content
|
|
return unless should_process_email_content?
|
|
|
|
@message.content_attributes ||= {}
|
|
email_attributes = build_email_attributes
|
|
@message.content_attributes[:email] = email_attributes
|
|
end
|
|
|
|
def process_email_string(email_string)
|
|
return [] if email_string.blank?
|
|
|
|
email_string.gsub(/\s+/, '').split(',')
|
|
end
|
|
|
|
def validate_email_addresses(all_emails)
|
|
all_emails&.each do |email|
|
|
raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
end
|
|
end
|
|
|
|
def message_type
|
|
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
|
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
|
end
|
|
|
|
@message_type
|
|
end
|
|
|
|
def sender
|
|
message_type == 'outgoing' ? (message_sender || @user) : @conversation.contact
|
|
end
|
|
|
|
def external_created_at
|
|
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
|
end
|
|
|
|
def automation_rule_id
|
|
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
|
end
|
|
|
|
def campaign_id
|
|
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
|
|
end
|
|
|
|
def template_params
|
|
@params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {}
|
|
end
|
|
|
|
def message_sender
|
|
return if @params[:sender_type] != 'AgentBot'
|
|
|
|
AgentBot.where(account_id: [nil, @conversation.account.id]).find_by(id: @params[:sender_id])
|
|
end
|
|
|
|
def message_params
|
|
{
|
|
account_id: @conversation.account_id,
|
|
inbox_id: @conversation.inbox_id,
|
|
message_type: message_type,
|
|
content: @params[:content],
|
|
private: @private,
|
|
sender: sender,
|
|
content_type: @params[:content_type],
|
|
items: @items,
|
|
in_reply_to: @in_reply_to,
|
|
echo_id: @params[:echo_id],
|
|
source_id: @params[:source_id]
|
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
|
end
|
|
|
|
def email_inbox?
|
|
@conversation.inbox&.inbox_type == 'Email'
|
|
end
|
|
|
|
def should_process_email_content?
|
|
email_inbox? && !@private && @message.content.present?
|
|
end
|
|
|
|
def build_email_attributes
|
|
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
|
|
normalized_content = normalize_email_body(@message.content)
|
|
|
|
# Use custom HTML content if provided, otherwise generate from message content
|
|
email_attributes[:html_content] = if custom_email_content_provided?
|
|
build_custom_html_content
|
|
else
|
|
build_html_content(normalized_content)
|
|
end
|
|
|
|
email_attributes[:text_content] = build_text_content(normalized_content)
|
|
email_attributes
|
|
end
|
|
|
|
def build_html_content(normalized_content)
|
|
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
|
|
rendered_html = render_email_html(normalized_content)
|
|
html_content[:full] = rendered_html
|
|
html_content[:reply] = rendered_html
|
|
html_content
|
|
end
|
|
|
|
def build_text_content(normalized_content)
|
|
text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {})
|
|
text_content[:full] = normalized_content
|
|
text_content[:reply] = normalized_content
|
|
text_content
|
|
end
|
|
|
|
def ensure_indifferent_access(hash)
|
|
return {} if hash.blank?
|
|
|
|
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
|
|
end
|
|
|
|
def normalize_email_body(content)
|
|
content.to_s.gsub("\r\n", "\n")
|
|
end
|
|
|
|
def render_email_html(content)
|
|
return '' if content.blank?
|
|
|
|
ChatwootMarkdownRenderer.new(content).render_message.to_s
|
|
end
|
|
|
|
def custom_email_content_provided?
|
|
@params[:email_html_content].present?
|
|
end
|
|
|
|
def build_custom_html_content
|
|
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
|
|
|
|
html_content[:full] = @params[:email_html_content]
|
|
html_content[:reply] = @params[:email_html_content]
|
|
|
|
html_content
|
|
end
|
|
end
|