مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2026-03-03 06:14:06 +00:00
Compare commits
15 الالتزامات
| المؤلف | SHA1 | التاريخ | |
|---|---|---|---|
|
|
eded789c37 | ||
|
|
ea542a0694 | ||
|
|
7e2acccd1e | ||
|
|
ee8d829a85 | ||
|
|
4fcb9e9a2e | ||
|
|
45dd8aaac5 | ||
|
|
364eba6c5f | ||
|
|
6a1ff56fe2 | ||
|
|
be456523dd | ||
|
|
3d208d632f | ||
|
|
1c67f72209 | ||
|
|
aa76aae232 | ||
|
|
f760cdb5a1 | ||
|
|
b55becd2ec | ||
|
|
92406129cf |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.2.1"
|
||||
".": "3.3.0"
|
||||
}
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -2,6 +2,46 @@
|
||||
|
||||
This file contains all the latest changes and updates to Postal.
|
||||
|
||||
## [3.3.0](https://github.com/postalserver/postal/compare/3.2.2...3.3.0) (2024-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **prometheus:** add `postal_message_queue_latency` metric ([ee8d829](https://github.com/postalserver/postal/commit/ee8d829a854f91e476167869cafe35c2d37bb314))
|
||||
* **worker:** allow number of threads to be configured ([7e2accc](https://github.com/postalserver/postal/commit/7e2acccd1ebd80750a3ebdb96cb5c36b5263cc24))
|
||||
* **worker:** scale connection pool with worker threads ([ea542a0](https://github.com/postalserver/postal/commit/ea542a0694b3465b04fd3ebc439837df414deb1e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **message-dequeuer:** ability to disable batching ([4fcb9e9](https://github.com/postalserver/postal/commit/4fcb9e9a2e34be5aa4bdf13f0529f40e564b72b4))
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **config-docs:** update docs for latest oidc defaults ([364eba6](https://github.com/postalserver/postal/commit/364eba6c5fce2f08a36489f42856ad5024a2062c))
|
||||
* **config-docs:** update proxy protocol to mention v1 ([45dd8aa](https://github.com/postalserver/postal/commit/45dd8aaac56f15481cb7bf9081401cb28dc1e707))
|
||||
|
||||
## [3.2.2](https://github.com/postalserver/postal/compare/3.2.1...3.2.2) (2024-03-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't use authentication on org & server deletion ([be45652](https://github.com/postalserver/postal/commit/be456523dd3aacb5c3eb45c9261da97ebffe603c))
|
||||
* **smtp-server:** fixes proxy protocol ([9240612](https://github.com/postalserver/postal/commit/92406129cfcf1a06499a6f5aa18c73f1d6195793))
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* allow config location message to be suppressed ([f760cdb](https://github.com/postalserver/postal/commit/f760cdb5a1d53e9c30ee495d129cbf12603a3cbd))
|
||||
* hide further config messages ([1c67f72](https://github.com/postalserver/postal/commit/1c67f72209c93404d7024ce3d15f6f54f2d707c4))
|
||||
* suppress config location on default-dkim-record ([aa76aae](https://github.com/postalserver/postal/commit/aa76aae2322af41af1bd60cfe1d69a11ac76324e))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* add tests for the legacy API ([3d208d6](https://github.com/postalserver/postal/commit/3d208d632f4fc8a4adbfdb2bf4b377271eae6692))
|
||||
|
||||
## [3.2.1](https://github.com/postalserver/postal/compare/3.2.0...3.2.1) (2024-03-13)
|
||||
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ class OrganizationsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless current_user.authenticate(params[:password])
|
||||
if params[:confirm_text].blank? || params[:confirm_text].downcase.strip != organization.name.downcase.strip
|
||||
respond_to do |wants|
|
||||
wants.html { redirect_to organization_delete_path(@organization), alert: "The password you entered was not valid. Please check and try again." }
|
||||
wants.json { render json: { alert: "The password you entered was invalid. Please check and try again." } }
|
||||
alert_text = "The text you entered does not match the organization name. Please check and try again."
|
||||
wants.html { redirect_to organization_delete_path(@organization), alert: alert_text }
|
||||
wants.json { render json: { alert: alert_text } }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@@ -65,17 +65,15 @@ class ServersController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless current_user.authenticate(params[:password])
|
||||
if params[:confirm_text].blank? || params[:confirm_text].downcase.strip != @server.name.downcase.strip
|
||||
respond_to do |wants|
|
||||
wants.html do
|
||||
redirect_to [:delete, organization, @server], alert: "The password you entered was not valid. Please check and try again."
|
||||
end
|
||||
wants.json do
|
||||
render json: { alert: "The password you entere was invalid. Please check and try again" }
|
||||
end
|
||||
alert_text = "The text you entered does not match the server name. Please check and try again."
|
||||
wants.html { redirect_to organization_delete_path(@organization), alert: alert_text }
|
||||
wants.json { render json: { alert: alert_text } }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@server.soft_destroy
|
||||
redirect_to_with_json organization_root_path(organization), notice: "#{@server.name} has been deleted successfully"
|
||||
end
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
module MessageDequeuer
|
||||
class InitialProcessor < Base
|
||||
|
||||
include HasPrometheusMetrics
|
||||
|
||||
attr_accessor :send_result
|
||||
|
||||
def process
|
||||
@@ -10,6 +12,7 @@ module MessageDequeuer
|
||||
logger.info "starting message unqueue"
|
||||
begin
|
||||
catch_stops do
|
||||
increment_dequeue_metric
|
||||
check_message_exists
|
||||
check_message_is_ready
|
||||
find_other_messages_for_batch
|
||||
@@ -17,7 +20,7 @@ module MessageDequeuer
|
||||
# Process the original message and then all of those
|
||||
# found for batching.
|
||||
process_message(@queued_message)
|
||||
@other_messages.each { |message| process_message(message) }
|
||||
@other_messages&.each { |message| process_message(message) }
|
||||
end
|
||||
ensure
|
||||
@state.finished
|
||||
@@ -28,6 +31,13 @@ module MessageDequeuer
|
||||
|
||||
private
|
||||
|
||||
def increment_dequeue_metric
|
||||
time_in_queue = Time.now.to_f - @queued_message.created_at.to_f
|
||||
log "queue latency is #{time_in_queue}s"
|
||||
observe_prometheus_histogram :postal_message_queue_latency,
|
||||
time_in_queue
|
||||
end
|
||||
|
||||
def check_message_exists
|
||||
return if @queued_message.message
|
||||
|
||||
@@ -45,6 +55,8 @@ module MessageDequeuer
|
||||
end
|
||||
|
||||
def find_other_messages_for_batch
|
||||
return unless Postal::Config.postal.batch_queued_messages?
|
||||
|
||||
@other_messages = @queued_message.batchable_messages(100)
|
||||
log "found #{@other_messages.size} associated messages to process at the same time", batch_key: @queued_message.batch_key
|
||||
rescue StandardError
|
||||
|
||||
@@ -116,18 +116,19 @@ module SMTPServer
|
||||
private
|
||||
|
||||
def proxy(data)
|
||||
# inet-protocol, client-ip, proxy-ip, client-port, proxy-port
|
||||
if m = data.match(/\APROXY (.+) (.+) (.+) (.+) (.+)\z/)
|
||||
@ip_address = m[2]
|
||||
check_ip_address
|
||||
@state = :welcome
|
||||
logger&.debug "\e[35mClient identified as #{@ip_address}\e[0m"
|
||||
increment_command_count("PROXY")
|
||||
"220 #{Postal::Config.postal.smtp_hostname} ESMTP Postal/#{id}"
|
||||
else
|
||||
@finished = true
|
||||
increment_error_count("proxy-error")
|
||||
"502 Proxy Error"
|
||||
return "220 #{Postal::Config.postal.smtp_hostname} ESMTP Postal/#{trace_id}"
|
||||
end
|
||||
|
||||
@finished = true
|
||||
increment_error_count("proxy-error")
|
||||
"502 Proxy Error"
|
||||
end
|
||||
|
||||
def quit
|
||||
|
||||
@@ -45,7 +45,9 @@ module Worker
|
||||
].freeze
|
||||
|
||||
# @param [Integer] thread_count The number of worker threads to run in this process
|
||||
def initialize(thread_count: 2, work_sleep_time: 5, task_sleep_time: 60)
|
||||
def initialize(thread_count: Postal::Config.worker.threads,
|
||||
work_sleep_time: 5,
|
||||
task_sleep_time: 60)
|
||||
@thread_count = thread_count
|
||||
@exit_pipe_read, @exit_pipe_write = IO.pipe
|
||||
@work_sleep_time = work_sleep_time
|
||||
@@ -58,6 +60,7 @@ module Worker
|
||||
def run
|
||||
logger.tagged(component: "worker") do
|
||||
setup_traps
|
||||
ensure_connection_pool_size_is_suitable
|
||||
start_work_threads
|
||||
start_tasks_thread
|
||||
wait_for_threads
|
||||
@@ -94,6 +97,23 @@ module Worker
|
||||
@exit_pipe_read.wait_readable(wait_time) ? true : false
|
||||
end
|
||||
|
||||
# Ensure that the connection pool is big enough for the number of threads
|
||||
# configured.
|
||||
#
|
||||
# @return [void]
|
||||
def ensure_connection_pool_size_is_suitable
|
||||
current_pool_size = ActiveRecord::Base.connection_pool.size
|
||||
desired_pool_size = @thread_count + 3
|
||||
|
||||
return if current_pool_size >= desired_pool_size
|
||||
|
||||
logger.warn "number of worker threads (#{@thread_count}) is more " \
|
||||
"than the db connection pool size (#{current_pool_size}+3), " \
|
||||
"increasing connection pool size to #{desired_pool_size}"
|
||||
|
||||
Postal.change_database_connection_pool_size(desired_pool_size)
|
||||
end
|
||||
|
||||
# Wait for all threads to complete
|
||||
#
|
||||
# @return [void]
|
||||
@@ -288,6 +308,9 @@ module Worker
|
||||
register_prometheus_histogram :postal_worker_task_runtime,
|
||||
docstring: "The time taken to process tasks",
|
||||
labels: [:task]
|
||||
|
||||
register_prometheus_histogram :postal_message_queue_latency,
|
||||
docstring: "The length of time between a message being queued and being dequeued"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -99,7 +99,7 @@ class OutgoingMessagePrototype
|
||||
{
|
||||
name: attachment[:name],
|
||||
content_type: attachment[:content_type] || "application/octet-stream",
|
||||
data: attachment[:base64] ? Base64.decode64(attachment[:data]) : attachment[:data]
|
||||
data: attachment[:base64] && attachment[:data] ? Base64.decode64(attachment[:data]) : attachment[:data]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
all its mail servers & data will be deleted from our systems.
|
||||
.dangerZone
|
||||
%p.pageContent__text.u-margin
|
||||
To continue to delete this organization, please enter your password in the field below and press
|
||||
To continue to delete this organization, please enter the name of the organization in the field below and press
|
||||
continue. <b class='u-red'>There will be no other confirmations.</b>
|
||||
= form_tag [organization, :delete], :method => :delete, :remote => true do
|
||||
= hidden_field_tag 'return_to', params[:return_to]
|
||||
%p.u-margin
|
||||
= password_field_tag "password", '', :class => 'input input--text input--danger'
|
||||
= text_field_tag "confirm_text", '', :class => 'input input--text input--danger'
|
||||
.buttonSet.u-center
|
||||
= submit_tag "Delete this organization, mail servers and all messages", :class => 'button button--danger js-form-submit'
|
||||
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
immediately.
|
||||
.dangerZone
|
||||
%p.pageContent__text.u-margin
|
||||
To continue to remove this server, please enter your password in the field below and press
|
||||
To continue to remove this server, please enter the server name in the field below and press
|
||||
continue. <b class='u-red'>There will be no other confirmations.</b>
|
||||
= form_tag [organization, @server], :remote => true, :method => :delete do
|
||||
= hidden_field_tag 'return_to', params[:return_to]
|
||||
%p.u-margin
|
||||
= password_field_tag "password", '', :class => 'input input--text input--danger'
|
||||
= text_field_tag "confirm_text", '', :class => 'input input--text input--danger'
|
||||
.buttonSet.u-center
|
||||
= submit_tag "Delete this mail server and all messages", :class => 'button button--danger'
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@ This document contains all the environment variables which are available for thi
|
||||
| `POSTAL_SMTP_RELAYS` | Array of strings | An array of SMTP relays in the format of smtp://host:port | [] |
|
||||
| `POSTAL_TRUSTED_PROXIES` | Array of strings | An array of IP addresses to trust for proxying requests to Postal (in addition to localhost addresses) | [] |
|
||||
| `POSTAL_QUEUED_MESSAGE_LOCK_STALE_DAYS` | Integer | The number of days after which to consider a lock as stale. Messages with stale locks will be removed and not retried. | 1 |
|
||||
| `POSTAL_BATCH_QUEUED_MESSAGES` | Boolean | When enabled queued messages will be de-queued in batches based on their destination | true |
|
||||
| `WEB_SERVER_DEFAULT_PORT` | Integer | The default port the web server should listen on unless overriden by the PORT environment variable | 5000 |
|
||||
| `WEB_SERVER_DEFAULT_BIND_ADDRESS` | String | The default bind address the web server should listen on unless overriden by the BIND_ADDRESS environment variable | 127.0.0.1 |
|
||||
| `WEB_SERVER_MAX_THREADS` | Integer | The maximum number of threads which can be used by the web server | 5 |
|
||||
| `WORKER_DEFAULT_HEALTH_SERVER_PORT` | Integer | The default port for the worker health server to listen on | 9090 |
|
||||
| `WORKER_DEFAULT_HEALTH_SERVER_BIND_ADDRESS` | String | The default bind address for the worker health server to listen on | 127.0.0.1 |
|
||||
| `WORKER_THREADS` | Integer | The number of threads to execute within each worker | 2 |
|
||||
| `MAIN_DB_HOST` | String | Hostname for the main MariaDB server | localhost |
|
||||
| `MAIN_DB_PORT` | Integer | The MariaDB port to connect to | 3306 |
|
||||
| `MAIN_DB_USERNAME` | String | The MariaDB username | postal |
|
||||
@@ -53,7 +55,7 @@ This document contains all the environment variables which are available for thi
|
||||
| `SMTP_SERVER_TLS_PRIVATE_KEY_PATH` | String | The path to the SMTP server's TLS private key | $config-file-root/smtp.key |
|
||||
| `SMTP_SERVER_TLS_CIPHERS` | String | Override ciphers to use for SSL | |
|
||||
| `SMTP_SERVER_SSL_VERSION` | String | The SSL versions which are supported | SSLv23 |
|
||||
| `SMTP_SERVER_PROXY_PROTOCOL` | Boolean | Enable proxy protocol for use behind some load balancers | false |
|
||||
| `SMTP_SERVER_PROXY_PROTOCOL` | Boolean | Enable proxy protocol for use behind some load balancers (supports proxy protocol v1 only) | false |
|
||||
| `SMTP_SERVER_LOG_CONNECTIONS` | Boolean | Enable connection logging | false |
|
||||
| `SMTP_SERVER_MAX_MESSAGE_SIZE` | Integer | The maximum message size to accept from the SMTP server (in MB) | 14 |
|
||||
| `SMTP_SERVER_LOG_IP_ADDRESS_EXCLUSION_MATCHER` | String | A regular expression to use to exclude connections from logging | |
|
||||
@@ -98,13 +100,14 @@ This document contains all the environment variables which are available for thi
|
||||
| `MIGRATION_WAITER_ATTEMPTS` | Integer | The number of attempts to try waiting for migrations to complete before start | 120 |
|
||||
| `MIGRATION_WAITER_SLEEP_TIME` | Integer | The number of seconds to wait between each migration check | 2 |
|
||||
| `OIDC_ENABLED` | Boolean | Enable OIDC authentication | false |
|
||||
| `OIDC_LOCAL_AUTHENTICATION_ENABLED` | Boolean | When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available. | true |
|
||||
| `OIDC_NAME` | String | The name of the OIDC provider as shown in the UI | OIDC Provider |
|
||||
| `OIDC_ISSUER` | String | The OIDC issuer URL | |
|
||||
| `OIDC_IDENTIFIER` | String | The client ID for OIDC | |
|
||||
| `OIDC_SECRET` | String | The client secret for OIDC | |
|
||||
| `OIDC_SCOPES` | Array of strings | Scopes to request from the OIDC server. | openid |
|
||||
| `OIDC_SCOPES` | Array of strings | Scopes to request from the OIDC server. | openid,email |
|
||||
| `OIDC_UID_FIELD` | String | The field to use to determine the user's UID | sub |
|
||||
| `OIDC_EMAIL_ADDRESS_FIELD` | String | The field to use to determine the user's email address | sub |
|
||||
| `OIDC_EMAIL_ADDRESS_FIELD` | String | The field to use to determine the user's email address | email |
|
||||
| `OIDC_NAME_FIELD` | String | The field to use to determine the user's name | name |
|
||||
| `OIDC_DISCOVERY` | Boolean | Enable discovery to determine endpoints from .well-known/openid-configuration from the Issuer | true |
|
||||
| `OIDC_AUTHORIZATION_ENDPOINT` | String | The authorize endpoint on the authorization server (only used when discovery is false) | |
|
||||
|
||||
@@ -31,6 +31,8 @@ postal:
|
||||
trusted_proxies: []
|
||||
# The number of days after which to consider a lock as stale. Messages with stale locks will be removed and not retried.
|
||||
queued_message_lock_stale_days: 1
|
||||
# When enabled queued messages will be de-queued in batches based on their destination
|
||||
batch_queued_messages: true
|
||||
|
||||
web_server:
|
||||
# The default port the web server should listen on unless overriden by the PORT environment variable
|
||||
@@ -45,6 +47,8 @@ worker:
|
||||
default_health_server_port: 9090
|
||||
# The default bind address for the worker health server to listen on
|
||||
default_health_server_bind_address: 127.0.0.1
|
||||
# The number of threads to execute within each worker
|
||||
threads: 2
|
||||
|
||||
main_db:
|
||||
# Hostname for the main MariaDB server
|
||||
@@ -113,7 +117,7 @@ smtp_server:
|
||||
tls_ciphers:
|
||||
# The SSL versions which are supported
|
||||
ssl_version: SSLv23
|
||||
# Enable proxy protocol for use behind some load balancers
|
||||
# Enable proxy protocol for use behind some load balancers (supports proxy protocol v1 only)
|
||||
proxy_protocol: false
|
||||
# Enable connection logging
|
||||
log_connections: false
|
||||
@@ -223,6 +227,8 @@ migration_waiter:
|
||||
oidc:
|
||||
# Enable OIDC authentication
|
||||
enabled: false
|
||||
# When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available.
|
||||
local_authentication_enabled: true
|
||||
# The name of the OIDC provider as shown in the UI
|
||||
name: OIDC Provider
|
||||
# The OIDC issuer URL
|
||||
@@ -233,11 +239,11 @@ oidc:
|
||||
secret:
|
||||
# Scopes to request from the OIDC server.
|
||||
scopes:
|
||||
- openid
|
||||
- openid,email
|
||||
# The field to use to determine the user's UID
|
||||
uid_field: sub
|
||||
# The field to use to determine the user's email address
|
||||
email_address_field: sub
|
||||
email_address_field: email
|
||||
# The field to use to determine the user's name
|
||||
name_field: name
|
||||
# Enable discovery to determine endpoints from .well-known/openid-configuration from the Issuer
|
||||
|
||||
@@ -22,6 +22,8 @@ module Postal
|
||||
|
||||
class << self
|
||||
|
||||
attr_writer :current_process_type
|
||||
|
||||
# Return the path to the config file
|
||||
#
|
||||
# @return [String]
|
||||
@@ -38,30 +40,36 @@ module Postal
|
||||
Dotenv.load(".env")
|
||||
sources << Konfig::Sources::Environment.new(ENV)
|
||||
|
||||
silence_config_messages = ENV.fetch("SILENCE_POSTAL_CONFIG_MESSAGES", "false") == "true"
|
||||
|
||||
# If a config file exists, we need to load that. Config files can
|
||||
# either be legacy (v1) or new (v2). Any file without a 'version'
|
||||
# key is a legacy file whereas new-style config files will include
|
||||
# the 'version: 2' key/value.
|
||||
if File.file?(config_file_path)
|
||||
puts "Loading config from #{config_file_path}"
|
||||
unless silence_config_messages
|
||||
warn "Loading config from #{config_file_path}"
|
||||
end
|
||||
|
||||
config_file = File.read(config_file_path)
|
||||
yaml = YAML.safe_load(config_file)
|
||||
config_version = yaml["version"] || 1
|
||||
case config_version
|
||||
when 1
|
||||
puts "WARNING: Using legacy config file format. Upgrade your postal.yml to use"
|
||||
puts "version 2 of the Postal configuration or configure using environment"
|
||||
puts "variables. See https://postalserver.io/config-v2 for details."
|
||||
unless silence_config_messages
|
||||
warn "WARNING: Using legacy config file format. Upgrade your postal.yml to use"
|
||||
warn "version 2 of the Postal configuration or configure using environment"
|
||||
warn "variables. See https://postalserver.io/config-v2 for details."
|
||||
end
|
||||
sources << LegacyConfigSource.new(yaml)
|
||||
when 2
|
||||
sources << Konfig::Sources::YAML.new(config_file)
|
||||
else
|
||||
raise "Invalid version specified in Postal config file. Must be 1 or 2."
|
||||
end
|
||||
else
|
||||
puts "No configuration file found at #{config_file_path}"
|
||||
puts "Only using environment variables for configuration"
|
||||
elsif !silence_config_messages
|
||||
warn "No configuration file found at #{config_file_path}"
|
||||
warn "Only using environment variables for configuration"
|
||||
end
|
||||
|
||||
# Build configuration with the provided sources.
|
||||
@@ -130,6 +138,21 @@ module Postal
|
||||
end
|
||||
end
|
||||
|
||||
# Change the connection pool size to the given size.
|
||||
#
|
||||
# @param new_size [Integer]
|
||||
# @return [void]
|
||||
def change_database_connection_pool_size(new_size)
|
||||
ActiveRecord::Base.connection_pool.disconnect!
|
||||
|
||||
config = ActiveRecord::Base.configurations
|
||||
.configs_for(env_name: Rails.env)
|
||||
.first
|
||||
.configuration_hash
|
||||
|
||||
ActiveRecord::Base.establish_connection(config.merge(pool: new_size))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Config = initialize_config
|
||||
|
||||
@@ -96,6 +96,11 @@ module Postal
|
||||
description "The number of days after which to consider a lock as stale. Messages with stale locks will be removed and not retried."
|
||||
default 1
|
||||
end
|
||||
|
||||
boolean :batch_queued_messages do
|
||||
description "When enabled queued messages will be de-queued in batches based on their destination"
|
||||
default true
|
||||
end
|
||||
end
|
||||
|
||||
group :web_server do
|
||||
@@ -125,6 +130,11 @@ module Postal
|
||||
description "The default bind address for the worker health server to listen on"
|
||||
default "127.0.0.1"
|
||||
end
|
||||
|
||||
integer :threads do
|
||||
description "The number of threads to execute within each worker"
|
||||
default 2
|
||||
end
|
||||
end
|
||||
|
||||
group :main_db do
|
||||
@@ -279,7 +289,7 @@ module Postal
|
||||
end
|
||||
|
||||
boolean :proxy_protocol do
|
||||
description "Enable proxy protocol for use behind some load balancers"
|
||||
description "Enable proxy protocol for use behind some load balancers (supports proxy protocol v1 only)"
|
||||
default false
|
||||
end
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ENV["SILENCE_POSTAL_CONFIG_LOCATION_MESSAGE"] = "true"
|
||||
require File.expand_path("../lib/postal/config", __dir__)
|
||||
puts Postal.rp_dkim_dns_record
|
||||
|
||||
118
spec/apis/legacy_api/messages/deliveries_spec.rb
Normal file
118
spec/apis/legacy_api/messages/deliveries_spec.rb
Normal file
@@ -0,0 +1,118 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Legacy Messages API", type: :request do
|
||||
describe "/api/v1/messages/deliveries" do
|
||||
context "when no authentication is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/deliveries"
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AccessDenied"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential does not match anything" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/deliveries", headers: { "x-server-api-key" => "invalid" }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "InvalidServerAPIKey"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential belongs to a suspended server" do
|
||||
it "returns an error" do
|
||||
server = create(:server, :suspended)
|
||||
credential = create(:credential, server: server)
|
||||
post "/api/v1/messages/deliveries", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "ServerSuspended"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential is valid" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
|
||||
context "when no message ID is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/deliveries", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "parameter-error"
|
||||
expect(parsed_body["data"]["message"]).to match(/`id` parameter is required but is missing/)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the message ID does not exist" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/deliveries",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: { id: 123 }.to_json
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "MessageNotFound"
|
||||
expect(parsed_body["data"]["id"]).to eq 123
|
||||
end
|
||||
end
|
||||
|
||||
context "when the message ID exists" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
let(:message) { MessageFactory.outgoing(server) }
|
||||
|
||||
before do
|
||||
message.create_delivery("SoftFail", details: "no server found",
|
||||
output: "404",
|
||||
sent_with_ssl: true,
|
||||
log_id: "1234",
|
||||
time: 1.2)
|
||||
message.create_delivery("Sent", details: "sent successfully",
|
||||
output: "200",
|
||||
sent_with_ssl: false,
|
||||
log_id: "5678",
|
||||
time: 2.2)
|
||||
end
|
||||
|
||||
before do
|
||||
post "/api/v1/messages/deliveries",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: { id: message.id }.to_json
|
||||
end
|
||||
|
||||
it "returns an array of deliveries" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match([
|
||||
{ "id" => kind_of(Integer),
|
||||
"status" => "SoftFail",
|
||||
"details" => "no server found",
|
||||
"output" => "404",
|
||||
"sent_with_ssl" => true,
|
||||
"log_id" => "1234",
|
||||
"time" => 1.2,
|
||||
"timestamp" => kind_of(Float) },
|
||||
{ "id" => kind_of(Integer),
|
||||
"status" => "Sent",
|
||||
"details" => "sent successfully",
|
||||
"output" => "200",
|
||||
"sent_with_ssl" => false,
|
||||
"log_id" => "5678",
|
||||
"time" => 2.2,
|
||||
"timestamp" => kind_of(Float) },
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
270
spec/apis/legacy_api/messages/message_spec.rb
Normal file
270
spec/apis/legacy_api/messages/message_spec.rb
Normal file
@@ -0,0 +1,270 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Legacy Messages API", type: :request do
|
||||
describe "/api/v1/messages/message" do
|
||||
context "when no authentication is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/message"
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AccessDenied"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential does not match anything" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/message", headers: { "x-server-api-key" => "invalid" }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "InvalidServerAPIKey"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential belongs to a suspended server" do
|
||||
it "returns an error" do
|
||||
server = create(:server, :suspended)
|
||||
credential = create(:credential, server: server)
|
||||
post "/api/v1/messages/message", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "ServerSuspended"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential is valid" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
|
||||
context "when no message ID is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/message", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "parameter-error"
|
||||
expect(parsed_body["data"]["message"]).to match(/`id` parameter is required but is missing/)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the message ID does not exist" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/messages/message",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: { id: 123 }.to_json
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "MessageNotFound"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the message ID exists" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
let(:message) { MessageFactory.outgoing(server) }
|
||||
let(:expansions) { [] }
|
||||
|
||||
before do
|
||||
post "/api/v1/messages/message",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: { id: message.id, _expansions: expansions }.to_json
|
||||
end
|
||||
|
||||
context "when no expansions are requested" do
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the status expansion is requested" do
|
||||
let(:expansions) { ["status"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"status" => { "held" => false,
|
||||
"hold_expiry" => nil,
|
||||
"last_delivery_attempt" => nil,
|
||||
"status" => "Pending" }
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the details expansion is requested" do
|
||||
let(:expansions) { ["details"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"details" => { "bounce" => false,
|
||||
"bounce_for_id" => 0,
|
||||
"direction" => "outgoing",
|
||||
"mail_from" => "test@example.com",
|
||||
"message_id" => message.message_id,
|
||||
"rcpt_to" => "john@example.com",
|
||||
"received_with_ssl" => nil,
|
||||
"size" => kind_of(String),
|
||||
"subject" => "An example message",
|
||||
"tag" => nil,
|
||||
"timestamp" => kind_of(Float) }
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the details expansion is requested" do
|
||||
let(:expansions) { ["inspection"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"inspection" => { "inspected" => false,
|
||||
"spam" => false,
|
||||
"spam_score" => 0.0,
|
||||
"threat" => false,
|
||||
"threat_details" => nil }
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the body expansions are requested" do
|
||||
let(:expansions) { %w[plain_body html_body] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"plain_body" => message.plain_body,
|
||||
"html_body" => message.html_body
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the attachments expansions is requested" do
|
||||
let(:message) do
|
||||
MessageFactory.outgoing(server) do |_, mail|
|
||||
mail.attachments["example.txt"] = "hello world!"
|
||||
end
|
||||
end
|
||||
let(:expansions) { ["attachments"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"attachments" => [
|
||||
{
|
||||
"content_type" => "text/plain",
|
||||
"data" => Base64.encode64("hello world!"),
|
||||
"filename" => "example.txt",
|
||||
"hash" => Digest::SHA1.hexdigest("hello world!"),
|
||||
"size" => 12
|
||||
},
|
||||
]
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the headers expansions is requested" do
|
||||
let(:expansions) { ["headers"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"headers" => message.headers
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the raw_message expansions is requested" do
|
||||
let(:expansions) { ["raw_message"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"raw_message" => Base64.encode64(message.raw_message)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the activity_entries expansions is requested" do
|
||||
let(:message) do
|
||||
MessageFactory.outgoing(server) do |msg|
|
||||
msg.create_load(double("request", ip: "1.2.3.4", user_agent: "user agent"))
|
||||
link = msg.create_link("https://example.com")
|
||||
link_id = msg.database.select(:links, where: { token: link }).first["id"]
|
||||
msg.database.insert(:clicks, {
|
||||
message_id: msg.id,
|
||||
link_id: link_id,
|
||||
ip_address: "1.2.3.4",
|
||||
user_agent: "user agent",
|
||||
timestamp: Time.now.to_f
|
||||
})
|
||||
end
|
||||
end
|
||||
let(:expansions) { ["activity_entries"] }
|
||||
|
||||
it "returns details about the message" do
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]).to match({
|
||||
"id" => message.id,
|
||||
"token" => message.token,
|
||||
"activity_entries" => {
|
||||
"loads" => [{
|
||||
"ip_address" => "1.2.3.4",
|
||||
"user_agent" => "user agent",
|
||||
"timestamp" => match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\z/)
|
||||
}],
|
||||
"clicks" => [{
|
||||
"url" => "https://example.com",
|
||||
"ip_address" => "1.2.3.4",
|
||||
"user_agent" => "user agent",
|
||||
"timestamp" => match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\z/)
|
||||
}]
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
234
spec/apis/legacy_api/send/message_spec.rb
Normal file
234
spec/apis/legacy_api/send/message_spec.rb
Normal file
@@ -0,0 +1,234 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Legacy Send API", type: :request do
|
||||
describe "/api/v1/send/message" do
|
||||
context "when no authentication is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/send/message"
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AccessDenied"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential does not match anything" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/send/message", headers: { "x-server-api-key" => "invalid" }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "InvalidServerAPIKey"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential belongs to a suspended server" do
|
||||
it "returns an error" do
|
||||
server = create(:server, :suspended)
|
||||
credential = create(:credential, server: server)
|
||||
post "/api/v1/send/message", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "ServerSuspended"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential is valid" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
let(:domain) { create(:domain, owner: server) }
|
||||
let(:default_params) do
|
||||
{
|
||||
to: ["test@example.com"],
|
||||
cc: ["cc@example.com"],
|
||||
bcc: ["bcc@example.com"],
|
||||
from: "test@#{domain.name}",
|
||||
sender: "sender@#{domain.name}",
|
||||
tag: "test-tag",
|
||||
reply_to: "reply@example.com",
|
||||
plain_body: "plain text",
|
||||
html_body: "<p>html</p>",
|
||||
attachments: [{ name: "test1.txt", content_type: "text/plain", data: Base64.encode64("hello world 1") },
|
||||
{ name: "test2.txt", content_type: "text/plain", data: Base64.encode64("hello world 2") },],
|
||||
headers: { "x-test-header-1" => "111", "x-test-header-2" => "222" },
|
||||
bounce: false,
|
||||
subject: "Test"
|
||||
}
|
||||
end
|
||||
let(:params) { default_params }
|
||||
|
||||
before do
|
||||
post "/api/v1/send/message",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: params.to_json
|
||||
end
|
||||
|
||||
context "when no recipients are provided" do
|
||||
let(:params) { default_params.merge(to: [], cc: [], bcc: []) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "NoRecipients"
|
||||
expect(parsed_body["data"]["message"]).to match(/there are no recipients defined to receive this message/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no content is provided" do
|
||||
let(:params) { default_params.merge(html_body: nil, plain_body: nil) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "NoContent"
|
||||
expect(parsed_body["data"]["message"]).to match(/there is no content defined for this e-mail/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the number of 'To' recipients exceeds the maximum" do
|
||||
let(:params) { default_params.merge(to: ["a@a.com"] * 51) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "TooManyToAddresses"
|
||||
expect(parsed_body["data"]["message"]).to match(/the maximum number of To addresses has been reached/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the number of 'CC' recipients exceeds the maximum" do
|
||||
let(:params) { default_params.merge(cc: ["a@a.com"] * 51) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "TooManyCCAddresses"
|
||||
expect(parsed_body["data"]["message"]).to match(/the maximum number of CC addresses has been reached/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the number of 'BCC' recipients exceeds the maximum" do
|
||||
let(:params) { default_params.merge(bcc: ["a@a.com"] * 51) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "TooManyBCCAddresses"
|
||||
expect(parsed_body["data"]["message"]).to match(/the maximum number of BCC addresses has been reached/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the 'From' address is missing" do
|
||||
let(:params) { default_params.merge(from: nil) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "FromAddressMissing"
|
||||
expect(parsed_body["data"]["message"]).to match(/the from address is missing and is required/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the 'From' address is not authorised" do
|
||||
let(:params) { default_params.merge(from: "test@another.com") }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "UnauthenticatedFromAddress"
|
||||
expect(parsed_body["data"]["message"]).to match(/the from address is not authorised to send mail from this server/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when an attachment is missing a name" do
|
||||
let(:params) { default_params.merge(attachments: [{ name: nil, content_type: "text/plain", data: Base64.encode64("hello world 1") }]) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AttachmentMissingName"
|
||||
expect(parsed_body["data"]["message"]).to match(/an attachment is missing a name/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when an attachment is missing data" do
|
||||
let(:params) { default_params.merge(attachments: [{ name: "test1.txt", content_type: "text/plain", data: nil }]) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AttachmentMissingData"
|
||||
expect(parsed_body["data"]["message"]).to match(/an attachment is missing data/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when an attachment entry is not a hash" do
|
||||
let(:params) { default_params.merge(attachments: [123, "string"]) }
|
||||
|
||||
it "continues as if it wasn't there" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to|
|
||||
message_id = parsed_body["data"]["messages"][rcpt_to]["id"]
|
||||
message = server.message(message_id)
|
||||
expect(message.attachments).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when given a complete email to send" do
|
||||
it "returns details of the messages created" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]["messages"]).to match({
|
||||
"test@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ },
|
||||
"cc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ },
|
||||
"bcc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }
|
||||
})
|
||||
end
|
||||
|
||||
it "adds an appropriate received header" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
message_id = parsed_body["data"]["messages"]["test@example.com"]["id"]
|
||||
message = server.message(message_id)
|
||||
expect(message.headers["received"].first).to match(/\Afrom api/)
|
||||
end
|
||||
|
||||
it "creates appropriate message objects" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to|
|
||||
message_id = parsed_body["data"]["messages"][rcpt_to]["id"]
|
||||
message = server.message(message_id)
|
||||
expect(message).to have_attributes(
|
||||
server: server,
|
||||
rcpt_to: rcpt_to,
|
||||
mail_from: params[:from],
|
||||
subject: params[:subject],
|
||||
message_id: kind_of(String),
|
||||
timestamp: kind_of(Time),
|
||||
domain_id: domain.id,
|
||||
credential_id: credential.id,
|
||||
bounce: false,
|
||||
tag: params[:tag],
|
||||
headers: hash_including("x-test-header-1" => ["111"],
|
||||
"x-test-header-2" => ["222"],
|
||||
"sender" => [params[:sender]],
|
||||
"to" => ["test@example.com"],
|
||||
"cc" => ["cc@example.com"],
|
||||
"reply-to" => ["reply@example.com"]),
|
||||
plain_body: params[:plain_body],
|
||||
html_body: params[:html_body],
|
||||
attachments: [
|
||||
have_attributes(content_type: /\Atext\/plain/, filename: "test1.txt", body: have_attributes(to_s: "hello world 1")),
|
||||
have_attributes(content_type: /\Atext\/plain/, filename: "test2.txt", body: have_attributes(to_s: "hello world 2")),
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
152
spec/apis/legacy_api/send/raw_spec.rb
Normal file
152
spec/apis/legacy_api/send/raw_spec.rb
Normal file
@@ -0,0 +1,152 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Legacy Send API", type: :request do
|
||||
describe "/api/v1/send/raw" do
|
||||
context "when no authentication is provided" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/send/raw"
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "AccessDenied"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential does not match anything" do
|
||||
it "returns an error" do
|
||||
post "/api/v1/send/raw", headers: { "x-server-api-key" => "invalid" }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "InvalidServerAPIKey"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential belongs to a suspended server" do
|
||||
it "returns an error" do
|
||||
server = create(:server, :suspended)
|
||||
credential = create(:credential, server: server)
|
||||
post "/api/v1/send/raw", headers: { "x-server-api-key" => credential.key }
|
||||
expect(response.status).to eq 200
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "error"
|
||||
expect(parsed_body["data"]["code"]).to eq "ServerSuspended"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the credential is valid" do
|
||||
let(:server) { create(:server) }
|
||||
let(:credential) { create(:credential, server: server) }
|
||||
let(:domain) { create(:domain, owner: server) }
|
||||
let(:data) do
|
||||
mail = Mail.new
|
||||
mail.to = "test1@example.com"
|
||||
mail.from = "test@#{domain.name}"
|
||||
mail.subject = "test"
|
||||
mail.text_part = Mail::Part.new
|
||||
mail.text_part.body = "plain text"
|
||||
mail.html_part = Mail::Part.new
|
||||
mail.html_part.content_type = "text/html; charset=UTF-8"
|
||||
mail.html_part.body = "<p>html</p>"
|
||||
mail
|
||||
end
|
||||
let(:default_params) do
|
||||
{
|
||||
mail_from: "test@#{domain.name}",
|
||||
rcpt_to: ["test1@example.com", "test2@example.com"],
|
||||
data: Base64.encode64(data.to_s),
|
||||
bounce: false
|
||||
}
|
||||
end
|
||||
let(:params) { default_params }
|
||||
|
||||
before do
|
||||
post "/api/v1/send/raw",
|
||||
headers: { "x-server-api-key" => credential.key,
|
||||
"content-type" => "application/json" },
|
||||
params: params.to_json
|
||||
end
|
||||
|
||||
context "when rcpt_to is not provided" do
|
||||
let(:params) { default_params.except(:rcpt_to) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "parameter-error"
|
||||
expect(parsed_body["data"]["message"]).to match(/`rcpt_to` parameter is required but is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when mail_from is not provided" do
|
||||
let(:params) { default_params.except(:mail_from) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "parameter-error"
|
||||
expect(parsed_body["data"]["message"]).to match(/`mail_from` parameter is required but is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when data is not provided" do
|
||||
let(:params) { default_params.except(:data) }
|
||||
|
||||
it "returns an error" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "parameter-error"
|
||||
expect(parsed_body["data"]["message"]).to match(/`data` parameter is required but is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no recipients are provided" do
|
||||
let(:params) { default_params.merge(rcpt_to: []) }
|
||||
|
||||
it "returns success but with no messages" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["status"]).to eq "success"
|
||||
expect(parsed_body["data"]["messages"]).to eq({})
|
||||
expect(parsed_body["data"]["message_id"]).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when a valid email is provided" do
|
||||
it "returns details of the messages created" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
expect(parsed_body["data"]["message_id"]).to be_a String
|
||||
expect(parsed_body["data"]["messages"]).to be_a Hash
|
||||
expect(parsed_body["data"]["messages"]).to match({
|
||||
"test1@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ },
|
||||
"test2@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }
|
||||
})
|
||||
end
|
||||
|
||||
it "creates appropriate message objects" do
|
||||
parsed_body = JSON.parse(response.body)
|
||||
["test1@example.com", "test2@example.com"].each do |rcpt_to|
|
||||
message_id = parsed_body["data"]["messages"][rcpt_to]["id"]
|
||||
message = server.message(message_id)
|
||||
expect(message).to have_attributes(
|
||||
server: server,
|
||||
rcpt_to: rcpt_to,
|
||||
mail_from: "test@#{domain.name}",
|
||||
subject: "test",
|
||||
message_id: kind_of(String),
|
||||
timestamp: kind_of(Time),
|
||||
domain_id: domain.id,
|
||||
credential_id: credential.id,
|
||||
bounce: false,
|
||||
headers: hash_including("to" => ["test1@example.com"]),
|
||||
plain_body: "plain text",
|
||||
html_body: "<p>html</p>",
|
||||
attachments: [],
|
||||
received_with_ssl: true,
|
||||
scope: "outgoing",
|
||||
raw_message: data.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -62,13 +62,28 @@ module MessageDequeuer
|
||||
@queued_message3 = create(:queued_message, message: @message3)
|
||||
end
|
||||
|
||||
it "calls the single message process for the initial message and all batchable messages" do
|
||||
[queued_message, @queued_message2, @queued_message3].each do |msg|
|
||||
expect(SingleMessageProcessor).to receive(:process).with(msg,
|
||||
logger: logger,
|
||||
state: processor.state)
|
||||
context "when postal.batch_queued_messages is enabled" do
|
||||
it "calls the single message process for the initial message and all batchable messages" do
|
||||
[queued_message, @queued_message2, @queued_message3].each do |msg|
|
||||
expect(SingleMessageProcessor).to receive(:process).with(msg,
|
||||
logger: logger,
|
||||
state: processor.state)
|
||||
end
|
||||
processor.process
|
||||
end
|
||||
end
|
||||
|
||||
context "when postal.batch_queued_messages is disabled" do
|
||||
before do
|
||||
allow(Postal::Config.postal).to receive(:batch_queued_messages?) { false }
|
||||
end
|
||||
|
||||
it "does not call the single message process more than once" do
|
||||
expect(SingleMessageProcessor).to receive(:process).once.with(queued_message,
|
||||
logger: logger,
|
||||
state: processor.state)
|
||||
processor.process
|
||||
end
|
||||
processor.process
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,4 +9,10 @@ RSpec.describe Postal do
|
||||
expect(Postal.signer.private_key.to_pem).to eq OpenSSL::PKey::RSA.new(File.read(Postal::Config.postal.signing_key_path)).to_pem
|
||||
end
|
||||
end
|
||||
|
||||
describe "#change_database_connection_pool_size" do
|
||||
it "changes the connection pool size" do
|
||||
expect { Postal.change_database_connection_pool_size(8) }.to change { ActiveRecord::Base.connection_pool.size }.from(5).to(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
28
spec/lib/smtp_server/client/proxy_spec.rb
Normal file
28
spec/lib/smtp_server/client/proxy_spec.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module SMTPServer
|
||||
|
||||
describe Client do
|
||||
let(:ip_address) { nil }
|
||||
subject(:client) { described_class.new(ip_address) }
|
||||
|
||||
describe "PROXY" do
|
||||
context "when the proxy header is sent correctly" do
|
||||
it "sets the IP address" do
|
||||
expect(client.handle("PROXY TCP4 1.1.1.1 2.2.2.2 1111 2222")).to eq "220 #{Postal::Config.postal.smtp_hostname} ESMTP Postal/#{client.trace_id}"
|
||||
expect(client.ip_address).to eq "1.1.1.1"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the proxy header is not valid" do
|
||||
it "returns an error" do
|
||||
expect(client.handle("PROXY TCP4")).to eq "502 Proxy Error"
|
||||
expect(client.finished?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,5 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: queued_messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# attempts :integer default(0)
|
||||
# batch_key :string(255)
|
||||
# domain :string(255)
|
||||
# locked_at :datetime
|
||||
# locked_by :string(255)
|
||||
# manual :boolean default(FALSE)
|
||||
# retry_after :datetime
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# ip_address_id :integer
|
||||
# message_id :integer
|
||||
# route_id :integer
|
||||
# server_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_queued_messages_on_domain (domain)
|
||||
# index_queued_messages_on_message_id (message_id)
|
||||
# index_queued_messages_on_server_id (server_id)
|
||||
#
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe QueuedMessage do
|
||||
|
||||
@@ -37,9 +37,16 @@ RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
config.include GeneralHelpers
|
||||
|
||||
# Before all request specs, set the hostname to the web hostname for
|
||||
# Postal otherwise it'll be www.example.com which will fail host
|
||||
# authorization checks.
|
||||
config.before(:each, type: :request) do
|
||||
host! Postal::Config.postal.web_hostname
|
||||
end
|
||||
|
||||
# Test that the factories are working as they should and then clean up before getting started on
|
||||
# the rest of the suite.
|
||||
config.before(:suite) do
|
||||
# Test that the factories are working as they should and then clean up before getting started on
|
||||
# the rest of the suite.
|
||||
DatabaseCleaner.start
|
||||
FactoryBot.lint
|
||||
ensure
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم