مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-11-30 21:32:30 +00:00
332 أسطر
9.8 KiB
Ruby
332 أسطر
9.8 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: servers
|
|
#
|
|
# id :integer not null, primary key
|
|
# organization_id :integer
|
|
# uuid :string(255)
|
|
# name :string(255)
|
|
# mode :string(255)
|
|
# ip_pool_id :integer
|
|
# created_at :datetime
|
|
# updated_at :datetime
|
|
# permalink :string(255)
|
|
# send_limit :integer
|
|
# deleted_at :datetime
|
|
# message_retention_days :integer
|
|
# raw_message_retention_days :integer
|
|
# raw_message_retention_size :integer
|
|
# allow_sender :boolean default(FALSE)
|
|
# token :string(255)
|
|
# send_limit_approaching_at :datetime
|
|
# send_limit_approaching_notified_at :datetime
|
|
# send_limit_exceeded_at :datetime
|
|
# send_limit_exceeded_notified_at :datetime
|
|
# spam_threshold :decimal(8, 2)
|
|
# spam_failure_threshold :decimal(8, 2)
|
|
# postmaster_address :string(255)
|
|
# suspended_at :datetime
|
|
# outbound_spam_threshold :decimal(8, 2)
|
|
# domains_not_to_click_track :text(65535)
|
|
# suspension_reason :string(255)
|
|
# log_smtp_data :boolean default(FALSE)
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_servers_on_organization_id (organization_id)
|
|
# index_servers_on_permalink (permalink)
|
|
# index_servers_on_token (token)
|
|
# index_servers_on_uuid (uuid)
|
|
#
|
|
|
|
class Server < ApplicationRecord
|
|
|
|
RESERVED_PERMALINKS = ['new', 'all', 'search', 'stats', 'edit', 'manage', 'delete', 'destroy', 'remove']
|
|
|
|
include HasUUID
|
|
include HasSoftDestroy
|
|
|
|
belongs_to :organization
|
|
belongs_to :ip_pool, :optional => true
|
|
has_many :domains, :dependent => :destroy, :as => :owner
|
|
has_many :credentials, :dependent => :destroy
|
|
has_many :smtp_endpoints, :dependent => :destroy
|
|
has_many :http_endpoints, :dependent => :destroy
|
|
has_many :address_endpoints, :dependent => :destroy
|
|
has_many :routes, :dependent => :destroy
|
|
has_many :queued_messages, :dependent => :delete_all
|
|
has_many :webhooks, :dependent => :destroy
|
|
has_many :webhook_requests, :dependent => :destroy
|
|
has_many :track_domains, :dependent => :destroy
|
|
has_many :ip_pool_rules, :dependent => :destroy, :as => :owner
|
|
|
|
MODES = ['Live', 'Development']
|
|
|
|
random_string :token, :type => :chars, :length => 6, :unique => true, :upper_letters_only => true
|
|
default_value :permalink, -> { name ? name.parameterize : nil}
|
|
default_value :send_limit, -> { 100 }
|
|
default_value :raw_message_retention_days, -> { 30 }
|
|
default_value :raw_message_retention_size, -> { 2048 }
|
|
default_value :message_retention_days, -> { 60 }
|
|
default_value :spam_threshold, -> { 5.0 }
|
|
default_value :spam_failure_threshold, -> { 20.0 }
|
|
|
|
validates :name, :presence => true, :uniqueness => {:scope => :organization_id}
|
|
validates :mode, :inclusion => {:in => MODES}
|
|
validates :permalink, :presence => true, :uniqueness => {:scope => :organization_id}, :format => {:with => /\A[a-z0-9\-]*\z/}, :exclusion => {:in => RESERVED_PERMALINKS}
|
|
validate :validate_ip_pool_belongs_to_organization
|
|
|
|
before_validation(:on => :create) do
|
|
self.token = self.token.downcase if self.token
|
|
self.outbound_spam_threshold = 3.0 if self.outbound_spam_threshold.blank?
|
|
end
|
|
|
|
after_create do
|
|
message_db.provisioner.provision
|
|
end
|
|
|
|
after_commit(:on => :destroy) do
|
|
message_db.provisioner.drop
|
|
end
|
|
|
|
def status
|
|
if self.suspended?
|
|
'Suspended'
|
|
else
|
|
self.mode
|
|
end
|
|
end
|
|
|
|
def full_permalink
|
|
"#{organization.permalink}/#{permalink}"
|
|
end
|
|
|
|
def suspended?
|
|
suspended_at.present? || organization.suspended?
|
|
end
|
|
|
|
def actual_suspension_reason
|
|
if suspended?
|
|
if suspended_at.nil?
|
|
organization.suspension_reason
|
|
else
|
|
self.suspension_reason
|
|
end
|
|
end
|
|
end
|
|
|
|
def accessible_by?(user)
|
|
organization.accessible_by?(user)
|
|
end
|
|
|
|
def to_param
|
|
permalink
|
|
end
|
|
|
|
def message_db
|
|
@message_db ||= Postal::MessageDB::Database.new(self.organization_id, self.id)
|
|
end
|
|
|
|
def message(id)
|
|
message_db.message(id)
|
|
end
|
|
|
|
def message_rate
|
|
@message_rate ||= message_db.live_stats.total(60, :types => [:incoming, :outgoing]) / 60.0
|
|
end
|
|
|
|
def held_messages
|
|
@held_messages ||= message_db.messages(:where => {:held => 1}, :count => true)
|
|
end
|
|
|
|
def throughput_stats
|
|
@throughput_stats ||= begin
|
|
incoming = message_db.live_stats.total(60, :types => [:incoming])
|
|
outgoing = message_db.live_stats.total(60, :types => [:outgoing])
|
|
outgoing_usage = send_limit ? (outgoing / send_limit.to_f) * 100 : 0
|
|
{
|
|
:incoming => incoming,
|
|
:outgoing => outgoing,
|
|
:outgoing_usage => outgoing_usage
|
|
}
|
|
end
|
|
end
|
|
|
|
def bounce_rate
|
|
@bounce_rate ||= begin
|
|
time = Time.now.utc
|
|
total_outgoing = 0.0
|
|
total_bounces = 0.0
|
|
message_db.statistics.get(:daily, [:outgoing, :bounces], time, 30).each do |date, stat|
|
|
total_outgoing += stat[:outgoing]
|
|
total_bounces += stat[:bounces]
|
|
end
|
|
total_outgoing == 0 ? 0 : (total_bounces / total_outgoing) * 100
|
|
end
|
|
end
|
|
|
|
def domain_stats
|
|
domains = Domain.where(:owner_id => self.id, :owner_type => 'Server').to_a
|
|
total, unverified, bad_dns = 0, 0, 0
|
|
domains.each do |domain|
|
|
total += 1
|
|
unverified += 1 unless domain.verified?
|
|
bad_dns += 1 if domain.verified? && !domain.dns_ok?
|
|
end
|
|
[total, unverified, bad_dns]
|
|
end
|
|
|
|
def webhook_hash
|
|
{
|
|
:uuid => self.uuid,
|
|
:name => self.name,
|
|
:permalink => self.permalink,
|
|
:organization => self.organization&.permalink
|
|
}
|
|
end
|
|
|
|
def send_volume
|
|
@send_volume ||= message_db.live_stats.total(60, :types => [:outgoing])
|
|
end
|
|
|
|
def send_limit_approaching?
|
|
send_volume >= self.send_limit * 0.90
|
|
end
|
|
|
|
def send_limit_exceeded?
|
|
send_volume >= self.send_limit
|
|
end
|
|
|
|
def send_limit_warning(type)
|
|
AppMailer.send("server_send_limit_#{type}", self).deliver
|
|
self.update_column("send_limit_#{type}_notified_at", Time.now)
|
|
WebhookRequest.trigger(self, "SendLimit#{type.to_s.capitalize}", :server => webhook_hash, :volume => self.send_volume, :limit => self.send_limit)
|
|
end
|
|
|
|
def queue_size
|
|
@queue_size ||= queued_messages.retriable.count
|
|
end
|
|
|
|
def stats
|
|
{
|
|
:queue => queue_size,
|
|
:held => self.held_messages,
|
|
:bounce_rate => self.bounce_rate,
|
|
:message_rate => self.message_rate,
|
|
:throughput => self.throughput_stats,
|
|
:size => self.message_db.total_size
|
|
}
|
|
end
|
|
|
|
def authenticated_domain_for_address(address)
|
|
return nil if address.blank?
|
|
address = Postal::Helpers.strip_name_from_address(address)
|
|
uname, domain_name = address.split('@', 2)
|
|
return nil unless uname
|
|
return nil unless domain_name
|
|
uname, _ = uname.split('+', 2)
|
|
|
|
# Check the server's domain
|
|
if domain = Domain.verified.order(:owner_type => :desc).where("(owner_type = 'Organization' AND owner_id = ?) OR (owner_type = 'Server' AND owner_id = ?)", self.organization_id, self.id).where(:name => domain_name).first
|
|
return domain
|
|
end
|
|
|
|
if any_domain = self.domains.verified.where(:use_for_any => true).order(:name).first
|
|
return any_domain
|
|
end
|
|
end
|
|
|
|
def find_authenticated_domain_from_headers(headers)
|
|
header_to_check = ['from']
|
|
header_to_check << 'sender' if self.allow_sender?
|
|
header_to_check.each do |header_name|
|
|
if headers[header_name].is_a?(Array)
|
|
values = headers[header_name]
|
|
else
|
|
values = [headers[header_name].to_s]
|
|
end
|
|
|
|
authenticated_domains = values.map { |v| authenticated_domain_for_address(v) }.compact
|
|
if authenticated_domains.size == values.size
|
|
return authenticated_domains.first
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
def suspend(reason)
|
|
self.suspended_at = Time.now
|
|
self.suspension_reason = reason
|
|
self.save!
|
|
AppMailer.server_suspended(self).deliver
|
|
end
|
|
|
|
def unsuspend
|
|
self.suspended_at = nil
|
|
self.suspension_reason = nil
|
|
self.save!
|
|
end
|
|
|
|
def validate_ip_pool_belongs_to_organization
|
|
if self.ip_pool && self.ip_pool_id_changed? && !self.organization.ip_pools.include?(self.ip_pool)
|
|
errors.add :ip_pool_id, "must belong to the organization"
|
|
end
|
|
end
|
|
|
|
def ip_pool_for_message(message)
|
|
if message.scope == 'outgoing'
|
|
[self, self.organization].each do |scope|
|
|
rules = scope.ip_pool_rules.order(:created_at => :desc)
|
|
rules.each do |rule|
|
|
if rule.apply_to_message?(message)
|
|
return rule.ip_pool
|
|
end
|
|
end
|
|
end
|
|
self.ip_pool
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def self.triggered_send_limit(type)
|
|
servers = where("send_limit_#{type}_at IS NOT NULL AND send_limit_#{type}_at > ?", 3.minutes.ago)
|
|
servers.where("send_limit_#{type}_notified_at IS NULL OR send_limit_#{type}_notified_at < ?", 1.hour.ago)
|
|
end
|
|
|
|
def self.send_send_limit_notifications
|
|
[:approaching, :exceeded].each_with_object({}) do |type, hash|
|
|
hash[type] = 0
|
|
servers = self.triggered_send_limit(type)
|
|
unless servers.empty?
|
|
servers.each do |server|
|
|
hash[type] += 1
|
|
server.send_limit_warning(type)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.[](id, extra = nil)
|
|
server = nil
|
|
if id.is_a?(String)
|
|
if id =~ /\A(\w+)\/(\w+)\z/
|
|
server = includes(:organization).where(:organizations => {:permalink => $1}, :permalink => $2).first
|
|
end
|
|
else
|
|
server = where(:id => id).first
|
|
end
|
|
|
|
if extra
|
|
if extra.is_a?(String)
|
|
server.domains.where(:name => extra.to_s).first
|
|
else
|
|
server.message(extra.to_i)
|
|
end
|
|
else
|
|
server
|
|
end
|
|
end
|
|
|
|
end
|