diff --git a/app/models/email_template.rb b/app/models/email_template.rb index dd7f20f6e..79ddc75d8 100644 --- a/app/models/email_template.rb +++ b/app/models/email_template.rb @@ -22,6 +22,7 @@ class EmailTemplate < ApplicationRecord BRANDED_LAYOUT_NAME = 'base'.freeze DEFAULT_LOCALE = 'en'.freeze + MAX_BODY_LENGTH = 256.kilobytes CONTENT_FOR_LAYOUT_PATTERN = /\{\{\s*content_for_layout\s*\}\}/ enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true @@ -34,6 +35,7 @@ class EmailTemplate < ApplicationRecord if: :installation_scoped? validates :name, uniqueness: { scope: %i[account_id template_type locale], conditions: -> { where(inbox_id: nil) } }, if: :account_scoped? validates :name, uniqueness: { scope: %i[inbox_id template_type locale] }, if: :inbox_scoped? + validates :body, length: { maximum: MAX_BODY_LENGTH } validate :validate_inbox_account validate :validate_liquid_body validate :validate_layout_slot, if: :layout? diff --git a/spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb b/spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb index a2c2571b0..8960d641f 100644 --- a/spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb @@ -85,6 +85,20 @@ RSpec.describe 'Branded Email Layout API', type: :request do expect(response.parsed_body['branded_email_layout']).to eq(layout) end + it 'updates account-scoped branded email layouts up to 100 KiB' do + account.enable_features!(:branded_email_templates) + slot = '{{ content_for_layout }}' + large_layout = "#{'a' * (100.kilobytes - slot.length)}#{slot}" + + patch "/api/v1/accounts/#{account.id}/branded_email_layout", + headers: admin.create_new_auth_token, + params: { branded_email_layout: large_layout }, + as: :json + + expect(response).to have_http_status(:success) + expect(EmailTemplate.account_branded_layout_template_for(account).body.length).to eq(100.kilobytes) + end + it 'clears account-scoped branded email layout when blank value is passed' do account.enable_features!(:branded_email_templates) create(:email_template, :layout, account: account, body: layout) diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 6a8055a04..3e9aa9faa 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -636,6 +636,22 @@ RSpec.describe 'Inboxes API', type: :request do expect(response.parsed_body['branded_email_layout']).to eq(layout) end + it 'rejects branded email layouts larger than 256 KiB' do + account.enable_features!(:branded_email_templates) + email_channel = create(:channel_email, account: account) + email_inbox = create(:inbox, channel: email_channel, account: account) + slot = '{{ content_for_layout }}' + large_layout = "#{'a' * (EmailTemplate::MAX_BODY_LENGTH - slot.length + 1)}#{slot}" + + patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}", + headers: admin.create_new_auth_token, + params: { branded_email_layout: large_layout }, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('is too long (maximum is 262144 characters)') + end + it 'rolls back branded email layout when inbox update fails' do account.enable_features!(:branded_email_templates) email_channel = create(:channel_email, account: account) diff --git a/spec/models/email_template_spec.rb b/spec/models/email_template_spec.rb index 8cc9e667f..f18423e62 100644 --- a/spec/models/email_template_spec.rb +++ b/spec/models/email_template_spec.rb @@ -68,6 +68,23 @@ RSpec.describe EmailTemplate do expect(template.errors[:body]).to include('must include {{ content_for_layout }}') end + it 'allows email templates up to 262,144 characters' do + slot = '{{ content_for_layout }}' + body = "#{'a' * (described_class::MAX_BODY_LENGTH - slot.length)}#{slot}" + template = build(:email_template, :layout, account: create(:account), body: body) + + expect(template).to be_valid + end + + it 'rejects email templates larger than 262,144 characters' do + slot = '{{ content_for_layout }}' + body = "#{'a' * (described_class::MAX_BODY_LENGTH - slot.length + 1)}#{slot}" + template = build(:email_template, :layout, account: create(:account), body: body) + + expect(template).not_to be_valid + expect(template.errors[:body]).to include('is too long (maximum is 262144 characters)') + end + it 'validates liquid syntax' do template = build(:email_template, body: '{{ broken ') diff --git a/swagger/definitions/request/account/branded_email_layout_payload.yml b/swagger/definitions/request/account/branded_email_layout_payload.yml index e14aeeb48..55a43509a 100644 --- a/swagger/definitions/request/account/branded_email_layout_payload.yml +++ b/swagger/definitions/request/account/branded_email_layout_payload.yml @@ -4,5 +4,6 @@ properties: type: - string - 'null' - description: Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override. + maxLength: 262144 + description: Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override. example: '{{ content_for_layout }}' diff --git a/swagger/definitions/request/inbox/update_payload.yml b/swagger/definitions/request/inbox/update_payload.yml index 859ad147d..730b796c1 100644 --- a/swagger/definitions/request/inbox/update_payload.yml +++ b/swagger/definitions/request/inbox/update_payload.yml @@ -113,8 +113,9 @@ properties: type: - string - 'null' + maxLength: 262144 description: | - Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`. + Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter. Available for: `Email` example: '{{ content_for_layout }}' diff --git a/swagger/swagger.json b/swagger/swagger.json index 74f843826..61aaf55a8 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -12804,7 +12804,8 @@ "string", "null" ], - "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`.\n\nAvailable for: `Email`\n", + "maxLength": 262144, + "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter.\n\nAvailable for: `Email`\n", "example": "{{ content_for_layout }}" }, "channel": { @@ -12842,7 +12843,8 @@ "string", "null" ], - "description": "Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override.", + "maxLength": 262144, + "description": "Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override.", "example": "{{ content_for_layout }}" } } diff --git a/swagger/tag_groups/application_swagger.json b/swagger/tag_groups/application_swagger.json index 279bb3aaf..1c72fe3e1 100644 --- a/swagger/tag_groups/application_swagger.json +++ b/swagger/tag_groups/application_swagger.json @@ -11311,7 +11311,8 @@ "string", "null" ], - "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`.\n\nAvailable for: `Email`\n", + "maxLength": 262144, + "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter.\n\nAvailable for: `Email`\n", "example": "{{ content_for_layout }}" }, "channel": { @@ -11349,7 +11350,8 @@ "string", "null" ], - "description": "Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override.", + "maxLength": 262144, + "description": "Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override.", "example": "{{ content_for_layout }}" } } diff --git a/swagger/tag_groups/client_swagger.json b/swagger/tag_groups/client_swagger.json index b354d995c..a11db82d1 100644 --- a/swagger/tag_groups/client_swagger.json +++ b/swagger/tag_groups/client_swagger.json @@ -4119,7 +4119,8 @@ "string", "null" ], - "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`.\n\nAvailable for: `Email`\n", + "maxLength": 262144, + "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter.\n\nAvailable for: `Email`\n", "example": "{{ content_for_layout }}" }, "channel": { @@ -4157,7 +4158,8 @@ "string", "null" ], - "description": "Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override.", + "maxLength": 262144, + "description": "Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override.", "example": "{{ content_for_layout }}" } } diff --git a/swagger/tag_groups/other_swagger.json b/swagger/tag_groups/other_swagger.json index 1d7e3bcfd..7a7b8b406 100644 --- a/swagger/tag_groups/other_swagger.json +++ b/swagger/tag_groups/other_swagger.json @@ -3534,7 +3534,8 @@ "string", "null" ], - "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`.\n\nAvailable for: `Email`\n", + "maxLength": 262144, + "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter.\n\nAvailable for: `Email`\n", "example": "{{ content_for_layout }}" }, "channel": { @@ -3572,7 +3573,8 @@ "string", "null" ], - "description": "Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override.", + "maxLength": 262144, + "description": "Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override.", "example": "{{ content_for_layout }}" } } diff --git a/swagger/tag_groups/platform_swagger.json b/swagger/tag_groups/platform_swagger.json index c114eff05..161e13438 100644 --- a/swagger/tag_groups/platform_swagger.json +++ b/swagger/tag_groups/platform_swagger.json @@ -4295,7 +4295,8 @@ "string", "null" ], - "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}`.\n\nAvailable for: `Email`\n", + "maxLength": 262144, + "description": "Liquid HTML layout for outbound email replies. Must include `{{ content_for_layout }}` and be 262,144 characters or shorter.\n\nAvailable for: `Email`\n", "example": "{{ content_for_layout }}" }, "channel": { @@ -4333,7 +4334,8 @@ "string", "null" ], - "description": "Account-scoped Liquid HTML layout for branded email replies. Blank or null removes the account override.", + "maxLength": 262144, + "description": "Account-scoped Liquid HTML layout for branded email replies. Maximum length is 262,144 characters. Blank or null removes the account override.", "example": "{{ content_for_layout }}" } }