مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-11-30 21:32:30 +00:00
refactor: move app/util/* to app/lib/
هذا الالتزام موجود في:
@@ -1,130 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DKIMHeader
|
||||
|
||||
def initialize(domain, message)
|
||||
if domain && domain.dkim_status == "OK"
|
||||
@domain_name = domain.name
|
||||
@dkim_key = domain.dkim_key
|
||||
@dkim_identifier = domain.dkim_identifier
|
||||
else
|
||||
@domain_name = Postal.config.dns.return_path
|
||||
@dkim_key = Postal.signing_key
|
||||
@dkim_identifier = Postal.config.dns.dkim_identifier
|
||||
end
|
||||
@domain = domain
|
||||
@message = message
|
||||
@raw_headers, @raw_body = @message.gsub(/\r?\n/, "\r\n").split(/\r\n\r\n/, 2)
|
||||
end
|
||||
|
||||
def dkim_header
|
||||
"DKIM-Signature: v=1; " + dkim_properties.join("\r\n\t") + signature.scan(/.{1,72}/).join("\r\n\t")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def headers
|
||||
@headers ||= @raw_headers.to_s.gsub(/\r?\n\s/, " ").split(/\r?\n/)
|
||||
end
|
||||
|
||||
def header_names
|
||||
normalized_headers.map { |h| h.split(":")[0].strip }
|
||||
end
|
||||
|
||||
def normalized_headers
|
||||
[].tap do |new_headers|
|
||||
dkim_headers = headers.select do |h|
|
||||
h.match(/
|
||||
^(
|
||||
from|sender|reply-to|subject|date|message-id|to|cc|mime-version|content-type|content-transfer-encoding|
|
||||
resent-to|resent-cc|resent-from|resent-sender|resent-message-id|in-reply-to|references|list-id|list-help|
|
||||
list-owner|list-unsubscribe|list-subscribe|list-post
|
||||
):/ix)
|
||||
end
|
||||
dkim_headers.each do |h|
|
||||
new_headers << normalize_header(h)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_header(content)
|
||||
content = content.dup
|
||||
|
||||
# From the DKIM RFC6376
|
||||
# https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.2
|
||||
|
||||
# Split the key and value.
|
||||
key, value = content.split(":", 2)
|
||||
|
||||
# Convert all header field names (not the header field values) to
|
||||
# lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
|
||||
key.downcase!
|
||||
|
||||
# Unfold all header field continuation lines as described in [RFC5322]
|
||||
value.gsub!(/\r?\n[ \t]+/, " ")
|
||||
|
||||
# Convert all sequences of one or more WSP characters to a single SP character.
|
||||
value.gsub!(/[ \t]+/, " ")
|
||||
|
||||
# Delete all WSP characters at the end of each unfolded header field value.
|
||||
value.gsub!(/[ \t]*\z/, "")
|
||||
|
||||
# Delete any WSP characters remaining after the colon separating the header field name from the header field value.
|
||||
value.gsub!(/\A[ \t]*/, "")
|
||||
|
||||
# Join together
|
||||
key + ":" + value
|
||||
end
|
||||
|
||||
def normalized_body
|
||||
@normalized_body ||= begin
|
||||
content = @raw_body.dup
|
||||
|
||||
# From the DKIM RFC6376
|
||||
# https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.4
|
||||
|
||||
# a. Reduce whitespace
|
||||
#
|
||||
# * Reduce all sequences of WSP within a line to a single SP character.
|
||||
content.gsub!(/[ \t]+/, " ")
|
||||
|
||||
# * Ignore all whitespace at the end of lines. Implementations MUST NOT
|
||||
# remove the CRLF at the end of the line.
|
||||
content.gsub!(/ \r\n/, "\r\n")
|
||||
|
||||
# b. Ignore all empty lines at the end of the message body.
|
||||
content.gsub!(/[ \r\n]*\z/, "")
|
||||
|
||||
content += "\r\n"
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def body_hash
|
||||
@body_hash ||= Base64.encode64(Digest::SHA256.digest(normalized_body)).strip
|
||||
end
|
||||
|
||||
def dkim_properties
|
||||
@dkim_properties ||= [].tap do |header|
|
||||
header << "a=rsa-sha256; c=relaxed/relaxed;"
|
||||
header << "d=#{@domain_name};"
|
||||
header << "s=#{@dkim_identifier}; t=#{Time.now.utc.to_i};"
|
||||
header << "bh=#{body_hash};"
|
||||
header << "h=#{header_names.join(':')};"
|
||||
header << "b="
|
||||
end
|
||||
end
|
||||
|
||||
def dkim_header_for_signing
|
||||
"dkim-signature:v=1; #{dkim_properties.join(' ')}"
|
||||
end
|
||||
|
||||
def signable_header_string
|
||||
(normalized_headers + [dkim_header_for_signing]).join("\r\n")
|
||||
end
|
||||
|
||||
def signature
|
||||
Base64.encode64(@dkim_key.sign(OpenSSL::Digest.new("SHA256"), signable_header_string)).gsub("\n", "")
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,149 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DNSResolver
|
||||
|
||||
attr_reader :nameservers
|
||||
attr_reader :timeout
|
||||
|
||||
def initialize(nameservers: nil, timeout: 5)
|
||||
@nameservers = nameservers
|
||||
@timeout = timeout
|
||||
end
|
||||
|
||||
# Return all A records for the given name
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<String>]
|
||||
def a(name)
|
||||
dns do |dns|
|
||||
dns.getresources(name, Resolv::DNS::Resource::IN::A).map do |s|
|
||||
s.address.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return all AAAA records for the given name
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<String>]
|
||||
def aaaa(name)
|
||||
dns do |dns|
|
||||
dns.getresources(name, Resolv::DNS::Resource::IN::AAAA).map do |s|
|
||||
s.address.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return all TXT records for the given name
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<String>]
|
||||
def txt(name)
|
||||
dns do |dns|
|
||||
dns.getresources(name, Resolv::DNS::Resource::IN::TXT).map do |s|
|
||||
s.data.to_s.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return all CNAME records for the given name
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<String>]
|
||||
def cname(name)
|
||||
dns do |dns|
|
||||
dns.getresources(name, Resolv::DNS::Resource::IN::CNAME).map do |s|
|
||||
s.name.to_s.downcase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return all MX records for the given name
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<Array<Integer, String>>]
|
||||
def mx(name)
|
||||
dns do |dns|
|
||||
records = dns.getresources(name, Resolv::DNS::Resource::IN::MX).map do |m|
|
||||
[m.preference.to_i, m.exchange.to_s]
|
||||
end
|
||||
records.sort do |a, b|
|
||||
if a[0] == b[0]
|
||||
[-1, 1].sample
|
||||
else
|
||||
a[0] <=> b[0]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return the effective nameserver names for a given domain name.
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [Array<String>]
|
||||
def effective_ns(name)
|
||||
records = []
|
||||
dns do |dns|
|
||||
parts = name.split(".")
|
||||
(parts.size - 1).times do |n|
|
||||
d = parts[n, parts.size - n + 1].join(".")
|
||||
|
||||
records = dns.getresources(d, Resolv::DNS::Resource::IN::NS).map do |s|
|
||||
s.name.to_s
|
||||
end
|
||||
|
||||
break if records.present?
|
||||
end
|
||||
end
|
||||
|
||||
records
|
||||
end
|
||||
|
||||
# Return the hostname for a given IP address.
|
||||
# Returns the IP address itself if no hostname can be determined.
|
||||
#
|
||||
# @param [String] ip_address
|
||||
# @return [String]
|
||||
def ip_to_hostname(ip_address)
|
||||
dns do |dns|
|
||||
dns.getname(ip_address)&.to_s
|
||||
end
|
||||
rescue Resolv::ResolvError
|
||||
ip_address
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dns
|
||||
kwargs = @nameservers ? { nameserver: @nameservers } : {}
|
||||
Resolv::DNS.open(**kwargs) do |dns|
|
||||
dns.timeouts = [@timeout, @timeout / 2]
|
||||
yield dns
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
# Return a resolver which will use the nameservers for the given domain
|
||||
#
|
||||
# @param [String] name
|
||||
# @return [DNSResolver]
|
||||
def for_domain(name)
|
||||
resolver = new
|
||||
nameservers = resolver.effective_ns(name)
|
||||
ips = nameservers.map do |ns|
|
||||
resolver.a(ns)
|
||||
end.flatten.uniq
|
||||
new(nameservers: ips)
|
||||
end
|
||||
|
||||
# Return a local resolver to use for lookups
|
||||
#
|
||||
# @return [DNSResolver]
|
||||
def local
|
||||
@local ||= new
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MessageDequeuer
|
||||
|
||||
class << self
|
||||
|
||||
def process(message, logger:)
|
||||
processor = InitialProcessor.new(message, logger: logger)
|
||||
processor.process
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class QueryString
|
||||
|
||||
def initialize(string)
|
||||
@string = string.strip + " "
|
||||
end
|
||||
|
||||
def [](value)
|
||||
hash[value.to_s]
|
||||
end
|
||||
|
||||
delegate :empty?, to: :hash
|
||||
|
||||
def hash
|
||||
@hash ||= @string.scan(/([a-z]+):\s*(?:(\d{2,4}-\d{2}-\d{2}\s\d{2}:\d{2})|"(.*?)"|(.*?))(\s|\z)/).each_with_object({}) do |(key, date, string_with_spaces, value), hash|
|
||||
if date
|
||||
actual_value = date
|
||||
elsif string_with_spaces
|
||||
actual_value = string_with_spaces
|
||||
elsif value == "[blank]"
|
||||
actual_value = nil
|
||||
else
|
||||
actual_value = value
|
||||
end
|
||||
|
||||
if hash.keys.include?(key.to_s)
|
||||
hash[key.to_s] = [hash[key.to_s]].flatten
|
||||
hash[key.to_s] << actual_value
|
||||
else
|
||||
hash[key.to_s] = actual_value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
المرجع في مشكلة جديدة
حظر مستخدم