1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2025-11-30 21:32:30 +00:00
الملفات
postal/app/models/server.rb
2024-02-10 22:59:30 +00:00

345 أسطر
10 KiB
Ruby
خام اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غير مرئية

هذا الملف يحتوي على أحرف Unicode غير مرئية لا يمكن التمييز بينها بعين الإنسان ولكن قد تتم معالجتها بشكل مختلف بواسطة الحاسوب. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

# frozen_string_literal: true
# == 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 = %w[new all search stats edit manage delete destroy remove].freeze
MODES = %w[Live Development].freeze
include HasUUID
include HasSoftDestroy
attr_accessor :provision_database
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
random_string :token, type: :chars, length: 6, unique: true, upper_letters_only: true
default_value :permalink, -> { name ? name.parameterize : nil }
default_value :raw_message_retention_days, -> { 30 }
default_value :raw_message_retention_size, -> { 2048 }
default_value :message_retention_days, -> { 60 }
default_value :spam_threshold, -> { Postal.config.general.default_spam_threshold }
default_value :spam_failure_threshold, -> { Postal.config.general.default_spam_failure_threshold }
validates :name, presence: true, uniqueness: { scope: :organization_id, case_sensitive: false }
validates :mode, inclusion: { in: MODES }
validates :permalink, presence: true, uniqueness: { scope: :organization_id, case_sensitive: false }, 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 = token.downcase if token
end
after_create do
unless provision_database == false
message_db.provisioner.provision
end
end
after_commit(on: :destroy) do
unless provision_database == false
message_db.provisioner.drop
end
end
def status
if suspended?
"Suspended"
else
mode
end
end
def full_permalink
"#{organization.permalink}/#{permalink}"
end
def suspended?
suspended_at.present? || organization.suspended?
end
def actual_suspension_reason
return unless suspended?
if suspended_at.nil?
organization.suspension_reason
else
suspension_reason
end
end
def to_param
permalink
end
def message_db
@message_db ||= Postal::MessageDB::Database.new(organization_id, id)
end
delegate :message, to: :message_db
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: true }, 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 |_, stat|
total_outgoing += stat[:outgoing]
total_bounces += stat[:bounces]
end
total_outgoing.zero? ? 0 : (total_bounces / total_outgoing) * 100
end
end
def domain_stats
domains = Domain.where(owner_id: id, owner_type: "Server").to_a
total = 0
unverified = 0
bad_dns = 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: uuid,
name: name,
permalink: permalink,
organization: organization&.permalink
}
end
def send_volume
@send_volume ||= message_db.live_stats.total(60, types: [:outgoing])
end
def send_limit_approaching?
send_limit && (send_volume >= send_limit * 0.90)
end
def send_limit_exceeded?
send_limit && send_volume >= send_limit
end
def send_limit_warning(type)
AppMailer.send("server_send_limit_#{type}", self).deliver
update_column("send_limit_#{type}_notified_at", Time.now)
WebhookRequest.trigger(self, "SendLimit#{type.to_s.capitalize}", server: webhook_hash, volume: send_volume, limit: send_limit)
end
def queue_size
@queue_size ||= queued_messages.retriable.count
end
def stats
{
queue: queue_size,
held: held_messages,
bounce_rate: bounce_rate,
message_rate: message_rate,
throughput: throughput_stats,
size: message_db.total_size
}
end
# Return the domain which can be used to authenticate emails sent from the given e-mail address.
#
#  @param address [String] an e-mail address
# @return [Domain, nil] the domain to use for authentication
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
# Find a verified domain which directly matches the domain name for the given address.
domain = Domain.verified
.order(owner_type: :desc)
.where("(owner_type = 'Organization' AND owner_id = ?) OR " \
"(owner_type = 'Server' AND owner_id = ?)", organization_id, id)
.where(name: domain_name)
.first
# If there is a matching domain, return it
return domain if domain
# Otherwise, we need to look to see if there is a domain configured which can be used as the authenticated
# domain for any domain. This will look for domains directly within the server and return that.
any_domain = domains.verified.where(use_for_any: true).order(:name).first
return any_domain if any_domain
# Return nil if we can't find anything suitable
nil
end
def find_authenticated_domain_from_headers(headers)
header_to_check = ["from"]
header_to_check << "sender" if 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
save!
AppMailer.server_suspended(self).deliver
end
def unsuspend
self.suspended_at = nil
self.suspension_reason = nil
save!
end
def validate_ip_pool_belongs_to_organization
return unless ip_pool && ip_pool_id_changed? && !organization.ip_pools.include?(ip_pool)
errors.add :ip_pool_id, "must belong to the organization"
end
def ip_pool_for_message(message)
return unless message.scope == "outgoing"
[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
ip_pool
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 = triggered_send_limit(type)
next if servers.empty?
servers.each do |server|
hash[type] += 1
server.send_limit_warning(type)
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: ::Regexp.last_match(1) }, permalink: ::Regexp.last_match(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