Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker-compose pull
- run: docker compose pull
env:
POSTAL_IMAGE: ghcr.io/postalserver/postal:ci-${{ github.sha }}
- run: docker-compose run postal sh -c 'bundle exec rspec'
- run: docker compose run postal sh -c 'bundle exec rspec'
env:
POSTAL_IMAGE: ghcr.io/postalserver/postal:ci-${{ github.sha }}

Expand Down
55 changes: 55 additions & 0 deletions app/assets/javascripts/application/application.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,58 @@ $ ->
credentialTypeInput = $('select#credential_type')
if credentialTypeInput.length
toggleCredentialInputs(credentialTypeInput.val())

handleWebhookOutputStyle = ->
$outputStyleSelect = $('.js-output-style-select')
$allEventsSelect = $('.js-all-events-select')
$allEventsField = $allEventsSelect.closest('.fieldSet__field')
$checkboxes = $('.js-event-checkbox')
$nonBounceEvents = $('.js-non-bounce-event')
$listmonkNotice = $('.js-listmonk-notice')

return unless $outputStyleSelect.length

updateForOutputStyle = ->
isListmonk = $outputStyleSelect.val() == 'listmonk'

if isListmonk
$listmonkNotice.show()
$allEventsSelect.hide()
$allEventsSelect.val('false').trigger('change')

# Disable all events except MessageBounced and ensure MessageBounced is checked
$checkboxes.each ->
$checkbox = $(this)
eventType = $checkbox.data('event')
if eventType != 'MessageBounced'
$checkbox.prop('disabled', true).prop('checked', false)
else
$checkbox.prop('checked', true)

# Gray out non-bounce event items
$nonBounceEvents.addClass('is-disabled').css
'opacity': '0.5'
'pointer-events': 'none'
else
$listmonkNotice.hide()
$allEventsSelect.show()
$checkboxes.prop('disabled', false)

$nonBounceEvents.removeClass('is-disabled').css
'opacity': '1'
'pointer-events': 'auto'

updateForOutputStyle()
$outputStyleSelect.on('change', updateForOutputStyle)

# Prevent form submission if listmonk is selected but no MessageBounced event
$('form').on 'submit', (e) ->
if $outputStyleSelect.val() == 'listmonk'
messageBounceChecked = $('input[data-event="MessageBounced"]').is(':checked')
unless messageBounceChecked
alert('Listmonk output style requires the MessageBounced event to be selected.')
e.preventDefault()
return false

$(document).on 'turbolinks:load', ->
handleWebhookOutputStyle()
86 changes: 52 additions & 34 deletions app/assets/stylesheets/application/components/_webhook_list.scss
Original file line number Diff line number Diff line change
@@ -1,68 +1,86 @@
.webhookList {
border-radius:4px;
overflow:hidden;
box-shadow:0 0 10px rgba(0,0,0,0.2);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}

.webhookList__item {
background:#fff;
padding:15px;
background: #fff;
padding: 15px;
}
.webhookList__item:nth-child(even) {
background:none;
background: none;
}

.webhookList__item + .webhookList__item {
border-top:1px solid lighten(#ccd4e0, 10%);
border-top: 1px solid lighten(#ccd4e0, 10%);
}

.webhookList__top {
display:flex;
align-items: center;
min-width:1px;
display: flex;
align-items: center;
min-width: 1px;
}

.webhookList__labels {
flex: 0 0 auto;
line-height:0;
margin-left:10px;
.label + .label {
margin-left:2px;
}
flex: 0 0 auto;
line-height: 0;
margin-left: 10px;
.label + .label {
margin-left: 2px;
}
}

.webhookList__name {
font-weight:600;
flex: 1 1 auto;
overflow:hidden;
text-overflow:ellipsis;
line-height:1.4;
font-weight: 600;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
}

.webhookList__bottom {
display:flex;
margin-top:3px;
font-size:12px;
display: flex;
margin-top: 3px;
font-size: 12px;
}

.webhookList__usageTime {
color:#999;
line-height:1.4;
flex: 1 1 auto;
color: #999;
line-height: 1.4;
flex: 1 1 auto;
}

.webhookList__links {
flex: 0 0 auto;
display:flex;
flex: 0 0 auto;
display: flex;
}

.webhookList__link {
a {
color:#999;
text-decoration: underline;
}
a {
color: #999;
text-decoration: underline;
}
}

.webhookList__link + .webhookList__link {
margin-left:12px;
margin-left: 12px;
}

.checkboxList__item.is-disabled {
background-color: #f8f9fa;
color: #6c757d;
}

.checkboxList__item.is-disabled .checkboxList__actualLabel {
color: #6c757d;
}

.checkboxList__item.is-disabled .checkBoxList__text {
color: #adb5bd;
}

.checkboxList__item.is-disabled input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
1 change: 1 addition & 0 deletions app/controllers/legacy_api/messages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def message
size: message.size,
bounce: message.bounce,
bounce_for_id: message.bounce_for_id,
bounce_type: message.bounce_type,
tag: message.tag,
received_with_ssl: message.received_with_ssl
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/webhooks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def history_request
private

def safe_params
params.require(:webhook).permit(:name, :url, :all_events, :enabled, events: [])
params.require(:webhook).permit(:name, :url, :all_events, :enabled, :output_style, events: [])
end

end
3 changes: 2 additions & 1 deletion app/lib/message_dequeuer/incoming_message_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def handle_bounces
original_messages = queued_message.message.original_messages
unless original_messages.empty?
queued_message.message.original_messages.each do |orig_msg|
queued_message.message.update(bounce_for_id: orig_msg.id, domain_id: orig_msg.domain_id)
queued_message.message.update(bounce_for_id: orig_msg.id, domain_id: orig_msg.domain_id, bounce_type: "soft")
create_delivery "Processed", details: "This has been detected as a bounce message for <msg:#{orig_msg.id}>."
orig_msg.bounce!(queued_message.message)
log "bounce linked with message #{orig_msg.id}"
Expand All @@ -51,6 +51,7 @@ def handle_bounces
# otherwise we'll drop at this point.
return unless queued_message.message.route_id.nil?

queued_message.message.update(bounce_type: "hard")
log "no source messages found, hard failing"
create_delivery "HardFail", details: "This message was a bounce but we couldn't link it with any outgoing message and there was no route for it."
remove_from_queue
Expand Down
2 changes: 2 additions & 0 deletions app/models/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# last_used_at :datetime
# all_events :boolean default(FALSE)
# enabled :boolean default(TRUE)
# output_style :string(255) default("postal")
# sign :boolean default(TRUE)
# created_at :datetime
# updated_at :datetime
Expand All @@ -31,6 +32,7 @@ class Webhook < ApplicationRecord

validates :name, presence: true
validates :url, presence: true, format: { with: /\Ahttps?:\/\/[a-z0-9\-._?=&\/+:%@]+\z/i, allow_blank: true }
validates :output_style, inclusion: { in: %w[postal listmonk], message: "%{value} is not a valid output style" }

scope :enabled, -> { where(enabled: true) }

Expand Down
58 changes: 48 additions & 10 deletions app/services/webhook_delivery_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class WebhookDeliveryService

def initialize(webhook_request:)
@webhook_request = webhook_request
@webhook = @webhook_request.webhook
end

def call
Expand All @@ -25,19 +26,36 @@ def success?
private

def generate_payload
@payload = {
event: @webhook_request.event,
timestamp: @webhook_request.created_at.to_f,
payload: @webhook_request.payload,
uuid: @webhook_request.uuid
}.to_json
case @webhook.output_style
when "listmonk"
payload_data = generate_listmonk_payload
if payload_data.nil?
raise Postal::Error, "Unsupported event '#{@webhook_request.event}' for output style 'listmonk'"
end
@payload = payload_data.to_json
else
@payload = {
event: @webhook_request.event,
timestamp: @webhook_request.created_at.to_f,
payload: @webhook_request.payload,
uuid: @webhook_request.uuid
}.to_json
end
end

def send_request
@http_result = Postal::HTTP.post(@webhook_request.url,
sign: true,
json: @payload,
timeout: 5)
options = {
sign: true,
json: @payload,
timeout: 5
}

if @webhook.output_style == 'listmonk'
options[:username] = Postal::Config.listmonk.api_user
options[:password] = Postal::Config.listmonk.api_key
end

@http_result = Postal::HTTP.post(@webhook_request.url, options)

@success = (@http_result[:code] >= 200 && @http_result[:code] < 300)
end
Expand Down Expand Up @@ -90,6 +108,26 @@ def update_webhook_request
@webhook_request.destroy!
end

def generate_listmonk_payload
case @webhook_request.event
when "MessageBounced"
payload_data = @webhook_request.payload
bounce_data = payload_data[:bounce] || payload_data["bounce"]
original_data = payload_data[:original_message] || payload_data["original_message"]

bounce_type = bounce_data[:bounce_type] || bounce_data["bounce_type"]
email = original_data[:to] || original_data["to"]

{
email: email,
source: "postal",
type: bounce_type == "soft" ? "soft" : "hard"
}
else
nil
end
end

def logger
Postal.logger
end
Expand Down
20 changes: 15 additions & 5 deletions app/views/webhooks/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,32 @@
You can enable or disable this webhook without fully removing it from the system. If there are any outstanding
webhook deliveries, they will still be completed even if disabled.

.fieldSet__field
= f.label :output_style, 'Output Style', :class => 'fieldSet__label'
.fieldSet__input
= f.select :output_style, [["Postal - Standard Postal webhook format", "postal"], ["Listmonk - Listmonk compatible format", "listmonk"]], {}, :class => 'input input--select js-output-style-select'
%p.fieldSet__text
Choose the format for webhook payloads. Postal format provides full event details, while Listmonk format
is compatible with Listmonk's webhook expectations for bounce handling.
%p.fieldSet__text.js-listmonk-notice{:style => "display: none; color: #856404; background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 8px; border-radius: 4px; margin-top: 8px;"}
%strong Note:
When Listmonk format is selected, only the MessageBounced event is supported. Other events will be automatically disabled.

.fieldSet__field
= f.label :all_events, 'Events', :class => 'fieldSet__label'
.fieldSet__input
= hidden_field_tag 'webhook[events][]'
= f.select :all_events, [["Yes - send all events to this URL", true], ["No - I'll choose which requests to send", false]], {},:class => 'input input--select fieldSet__checkboxListAfter js-checkbox-list-toggle'
= f.select :all_events, [["Yes - send all events to this URL", true], ["No - I'll choose which requests to send", false]], {},:class => 'input input--select fieldSet__checkboxListAfter js-checkbox-list-toggle js-all-events-select'
%ul.checkboxList{:class => [@webhook.all_events? ? 'is-hidden' : '']}
- for event in WebhookEvent::EVENTS
%li.checkboxList__item
.checkboxList__checkbox= check_box_tag "webhook[events][]", event, @webhook.events.include?(event), :id => "event_#{event}"
%li.checkboxList__item{:class => (event != 'MessageBounced' ? 'js-non-bounce-event' : '')}
.checkboxList__checkbox= check_box_tag "webhook[events][]", event, @webhook.events.include?(event), :id => "event_#{event}", :class => 'js-event-checkbox', :data => {:event => event}
.checkboxList__label
= label_tag "event_#{event}", event, :class => 'checkboxList__actualLabel checkboxList__devEvent'
= label_tag "event_#{event}", event, :class => 'checkboxList__actualLabel checkboxList__devEvent js-event-label'
%p.checkBoxList__text= t("webhook_events.#{event.underscore}")

.fieldSetSubmit.buttonSet
= f.submit @webhook.new_record? ? "Create Webhook" : "Save Webhook", :class => 'button button--positive js-form-submit'
.fieldSetSubmit__delete
- if f.object.persisted?
= link_to "Delete Webhook", [organization, @server, @webhook], :remote => true, :class => 'button button--danger', :method => :delete, :data => {:confirm => "Are you sure you wish to delete this webhook?"}

4 changes: 4 additions & 0 deletions app/views/webhooks/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
%span.label.label--green Enabled
- else
%span.label.label--red Disabled
- if webhook.output_style == 'listmonk'
%span.label.label--blue Listmonk
- else
%span.label.label--gray Postal
.webhookList__bottom
%p.webhookList__usageTime
- if webhook.last_used_at
Expand Down
2 changes: 1 addition & 1 deletion db/migrate/20161003195209_create_authie_sessions.authie.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# This migration comes from authie (originally 20141012174250)
class CreateAuthieSessions < ActiveRecord::Migration
class CreateAuthieSessions < ActiveRecord::Migration[7.0]

def change
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# This migration comes from authie (originally 20141013115205)
class AddIndexesToAuthieSessions < ActiveRecord::Migration
class AddIndexesToAuthieSessions < ActiveRecord::Migration[7.0]

def change
end
Expand Down
Loading
Loading