Compare commits

...

3 Commits

Author SHA1 Message Date
Arthur POULET b05502fedf
Remove a binding.pry 2022-11-24 20:46:13 +01:00
Arthur POULET 7573060bbd
Improve logging and coding style 2022-11-24 20:44:59 +01:00
Arthur POULET 874660477e
Add unit tests 2022-11-24 18:56:08 +01:00
16 changed files with 175 additions and 74 deletions

View File

@ -28,3 +28,5 @@ gem "semver", "~> 1.0"
group :develop do
gem "pry", "~> 0.14.1"
end
gem "mocha", "~> 2.0"

View File

@ -10,6 +10,8 @@ GEM
macaddr (1.7.2)
systemu (~> 2.6.5)
method_source (1.0.0)
mocha (2.0.2)
ruby2_keywords (>= 0.0.5)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
net-imap (0.3.1)
@ -53,6 +55,7 @@ DEPENDENCIES
colorize (~> 0.8.1)
dotenv (~> 2.8)
erb (~> 3.0)
mocha (~> 2.0)
net-imap (~> 0.3.1)
net-smtp (~> 0.3.3)
pry (~> 0.14.1)

12
Rakefile Normal file
View File

@ -0,0 +1,12 @@
require "minitest/test_task"
Minitest::TestTask.create(:test) do |t|
t.libs << "test"
t.libs << "src"
t.warning = false
t.test_globs = ["test/**/test_*.rb"]
end
task :default => :test
task default: :test

View File

@ -4,4 +4,13 @@ $LOAD_PATH << File.join(Dir.pwd, "lib")
require "app"
require "distributor"
Signal.trap("SIGINT") do
Thread.new do
$logger.info "SIGINT, closing the app peacefully"
$logger.close
exit 0
end.join
end
$logger.info "Starting app"
Distributor.new.start(cpu_sleep: (ENV["CPU_SLEEP"] || 1).to_i)

View File

@ -16,3 +16,4 @@ DB_URL="sqlite://dev.db"
PORT=10081
DEBUG=false
CPU_CYCLE=1
LOG_FILE=/var/log/mailinglistrb.log

View File

@ -4,7 +4,10 @@ class Distributor
def self.parse(subject:, body:)
new = Attributes.new
subject_added_attribute = false
subject.to_s.split(",").each { new.add_attribute!(_1); subject_added_attribute = true }
subject.to_s.split(",").each do
new.add_attribute!(_1)
subject_added_attribute = true
end
body.to_s.split("\r\n").each { new.add_attribute!(_1) } if body.include?("=") && subject_added_attribute == false
new
end
@ -38,7 +41,7 @@ class Distributor
attributes = Attributes.parse(subject: subject_attributes, body: mail.body)
handler = @handlers[subject] || @handlers[:default]
$logger.info "#{handler.class}#handle on #{list.email} for #{mail.from}"
handler.handle(list: list, to: mail, attributes: attributes)
handler.handle(list:, to: mail, attributes:)
end
mail.seen!(imap_client: @imap_client)
end
@ -48,7 +51,7 @@ class Distributor
end
def start(cpu_sleep: 1)
puts "fetching new mail to distribute every #{cpu_sleep} second..."
$logger.info "fetching new mail to distribute every #{cpu_sleep} second..."
loop do
mail = @imap_client.fetch
handle_one(mail) if mail

View File

@ -35,7 +35,11 @@ class Distributor
user.permissions = permissions
user.save
body = SET_PERMISSIONS_TEMPLATE.result binding
@distributor.distribute(Protocols::Mail.build(subject: "ML #{list.name} subscription update", list: list, to: user.email, body: body))
@distributor.distribute(
Protocols::Mail.build(
subject: "ML #{list.name} subscription update", list:, to: user.email, body:
)
)
end
end

View File

@ -11,42 +11,46 @@ class Distributor
register =
begin
Email.register!(mailinglist: list, name: to.from_name, email: to.from).save
rescue => err
$logger.error err.message
rescue StandardError => e
$logger.error e.message
nil
end
if register
if !register.reader?
handle_wait_validation(list: list, to: to, register: register)
handle_wait_validation(list:, to:, register:)
else
handle_subscribed(list: list, to: to, register: register)
handle_subscribed(list:, to:, register:)
end
else
handle_403(list: list, to: to)
handle_403(list:, to:)
end
end
def handle_wait_validation(list:, to:, register:)
$logger.debug "Subscribe#handle_wait_validation on #{list.email} for #{to.from}"
body = WAIT_USER_TEMPLATE.result binding
@distributor.distribute(to.to_response(list: list, to: to, body: body))
@distributor.distribute(to.to_response(list:, to:, body:))
modo = list.enabled_modos.first
body = WAIT_MODO_TEMPLATE.result binding
@distributor.distribute(Protocols::Mail.build(subject: "ML #{list.name} requires validaton for #{to.from}", list: list, to: modo.email, body: body))
@distributor.distribute(
Protocols::Mail.build(
subject: "ML #{list.name} requires validaton for #{to.from}", list:, to: modo.email, body:,
),
)
end
def handle_subscribed(list:, to:, register:)
$logger.debug "Subscribe#handle_subscribed on #{list.email} for #{to.from}"
$logger.debug register.inspect
body = SUCCESS_TEMPLATE.result binding
@distributor.distribute(to.to_response(list: list, to: to, body: body))
@distributor.distribute(to.to_response(list:, to:, body:))
end
def handle_403(list:, to:)
$logger.debug "Subscribe#handle_403 on #{list.email} for #{to.from}"
body = FORBIDDEN_TEMPLATE.result binding
@distributor.distribute(to.to_response(list: list, to: to, body: body))
@distributor.distribute(to.to_response(list:, to:, body:))
end
end
@ -56,7 +60,7 @@ class Distributor
def handle(list:, to:, attributes:)
Email.unregister!(mailinglist: list, email: to.from)
body = SUCCESS_TEMPLATE.result binding
@distributor.distribute(to.to_response(list: list, to: to, body: body))
@distributor.distribute(to.to_response(list:, to:, body:))
end
end
@ -64,7 +68,7 @@ class Distributor
HELP_TEMPLATE = Actions.template("help")
def handle(list:, to:, attributes:)
body = HELP_TEMPLATE.result binding
@distributor.distribute(to.to_response(list: list, to: to, body: body))
@distributor.distribute(to.to_response(list:, to:, body:))
end
end
@ -72,12 +76,12 @@ class Distributor
class Distribute < Action
def handle(list:, to:, attributes:)
if !list
warn "invalid email writer for #{mail.from} on #{mail.to}"
$logger.warn "invalid email writer for #{mail.from} on #{mail.to}"
return nil
end
list.enabled_readers.each do |reader|
to_distrib = to.to_redistribute(list: list, dest: reader)
to_distrib = to.to_redistribute(list:, dest: reader)
@distributor.distribute(to_distrib)
end
end

View File

@ -1,21 +1,31 @@
require "logger"
$logger = Logger.new(STDOUT)
class MultiIO
def initialize(*targets)
@targets = targets
end
def <<(io)
@targets << io unless io == self
self
end
def write(*args)
@targets.each { _1.write(*args) }
self
end
def close
@targets.each(&:close)
end
end
log_output = MultiIO.new(STDOUT)
if ENV["LOG_FILE"]
log_file = File.open(ENV["LOG_FILE"], "a")
log_output << log_file
end
$logger = Logger.new(log_output)
$logger.level = Logger::INFO
$logger.level = Logger::DEBUG if $debug
def warn(*args, &block)
$logger.warn(*args, &block)
end
def puts(*args, &block)
$logger.info(*args, &block)
end
def error(*args, &block)
$logger.error(*args, &block)
end
def info(*args, &block)
$logger.info(*args, &block)
end

View File

@ -32,7 +32,7 @@ class Email < Sequel::Model($db)
none: NONE,
}
def self.from_symbols(*syms)
syms.map { FROM_SYMBOLS[_1] }.sum rescue binding.pry
syms.map { FROM_SYMBOLS[_1] }.sum
end
def self.op?(perm)

View File

@ -24,16 +24,16 @@ class Protocols::Imap
return nil if imap_mail.nil?
$logger.debug imap_mail.attr["RFC822.HEADER"]
mail = Protocols::Mail.new(imap_mail: imap_mail)
puts "READ #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
mail = Protocols::Mail.new(imap_mail:)
$logger.info "READ #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
mail
end
# Mark all existing messages as SEEN. See {#seen!}
def see_all_messages!
puts "MARK ALL NOT SEEN as SEEN"
@imap.search(["NOT", "SEEN"]).each { @imap.store _1, "+FLAGS", [Net::IMAP::SEEN] }
$logger.info "MARK ALL NOT SEEN as SEEN"
@imap.search(%w[NOT SEEN]).each { @imap.store _1, "+FLAGS", [Net::IMAP::SEEN] }
end
# Add the SEEN flag to a given email.
@ -41,7 +41,7 @@ class Protocols::Imap
#
# @param imap_mail [Protocols::Mail]
def seen!(imap_mail)
puts "MARK #{imap_mail.attr['UID']} as SEEN"
$logger.info "MARK #{imap_mail.attr['UID']} as SEEN"
@imap.uid_store imap_mail.attr["UID"], "+FLAGS", [Net::IMAP::SEEN]
end
end

View File

@ -21,18 +21,19 @@ class Protocols::Mail
# - Protocols::BODYTEXT
def initialize(imap_mail: nil)
if imap_mail
@imap_mail = imap_mail
envelope = imap_mail.attr[Protocols::ENVELOPE]
body_head = imap_mail.attr[Protocols::HEADERS]
body_text = imap_mail.attr[Protocols::BODYTEXT]
@from_name = envelope.from.first.name
@to_name = envelope.to.first.name
@from = "#{envelope.from.first.mailbox}@#{envelope.from.first.host}"
@to = "#{envelope.to.first.mailbox}@#{envelope.to.first.host}"
@subject = envelope.subject
@body = body_text
@headers = parse_rfc822_headers!(body_head)
@seq = header("X-Sequence").to_i
@imap_mail = imap_mail
envelope = imap_mail.attr[Protocols::ENVELOPE]
body_head = imap_mail.attr[Protocols::HEADERS]
body_text = imap_mail.attr[Protocols::BODYTEXT]
@from_name = envelope.from.first.name
@to_name = envelope.to.first.name
@from = "#{envelope.from.first.mailbox}@#{envelope.from.first.host}"
@to = "#{envelope.to.first.mailbox}@#{envelope.to.first.host}"
@subject = envelope.subject
@body = body_text
@headers = Protocols::Mail.parse_rfc822_headers(body_head)
@headers.filter! { HEADERS_KEEP.include?(_1[0]) }
@seq = header("X-Sequence").to_i
@message_id = header("Message-id")
end
end
@ -145,7 +146,7 @@ class Protocols::Mail
end
# :nodoc:
private def parse_rfc822_headers!(rfc822)
def self.parse_rfc822_headers(rfc822)
headers = []
rfc822.split("\r\n").each do |line|
if line[0] == "\t"
@ -155,7 +156,7 @@ class Protocols::Mail
headers << [k, v.to_s.strip]
end
end
headers.filter! { HEADERS_KEEP.include?(_1[0]) }
headers
end
# Get a header by key. Case insensitive. Only return the first occurence.

View File

@ -9,13 +9,13 @@ class Protocols::Smtp
# @param mail [Protocols::Mail]
def distribute(mail)
puts "SEND #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
$logger.info "SEND #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
smtp_raw = mail.to_smtp
$logger.debug smtp_raw.join("=====")
send_message_safe(*smtp_raw)
rescue => err
warn "FAILED #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
$logger.debug err
rescue StandardError => e
$logger.warn "FAILED #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
$logger.debug e
end
# :nodoc:
@ -31,14 +31,12 @@ class Protocols::Smtp
# :nodoc:
private def send_message_safe(*raw, max_tries: 3)
if max_tries == 0
return $logger.error("send_message_safe reached max_tries_limit")
end
return $logger.error("send_message_safe reached max_tries_limit") if max_tries == 0
begin
@smtp.send_message(*raw)
rescue EOFError, Net::SMTPServerBusy => err
warn err.message
rescue EOFError, Net::SMTPServerBusy => e
$logger.warn e.message
reset_smtp_client!
send_message_safe(*raw, max_tries: max_tries - 1)
end

View File

@ -8,32 +8,32 @@ class Semver
end
def to_s
"v#{@values.join(".")}"
"v#{@values.join('.')}"
end
def <=>(left)
def <=>(other)
cmp = 0
@values.each_with_index do |value, index|
cmp = value - left[index]
cmp = value - other[index]
break if cmp != 0
end
cmp
end
def >(left)
(self <=> left) > 0
def >(other)
(self <=> other) > 0
end
def <(left)
(self <=> left) < 0
def <(other)
(self <=> other) < 0
end
def ==(left)
(self <=> left) == 0
def ==(other)
(self <=> other) == 0
end
def !=(left)
(self <=> left) != 0
def !=(other)
(self <=> other) != 0
end
end

View File

@ -0,0 +1,51 @@
require "test_helper"
require "protocols"
module Protocols
class MailTest < Minitest::Test
def setup
@headers = <<~MAILEND
From: SenderH <sender@head.local>\r
To: DestH <dest@head.local>\r
Subject: -subject.head-\r
Date: #{DateTime.parse('2022-02-03T12:30:00Z').strftime(Protocols::Mail::DATE_FORMAT)}\r
Content-Type: plain/text\r
User-Agent: minitest\r
MAILEND
@imap_mail = OpenStruct.new(
attr: {
Protocols::ENVELOPE => OpenStruct.new(
from: [OpenStruct.new(mailbox: "sender", host: "local", name: "Sender")],
to: [OpenStruct.new(mailbox: "dest", host: "local", name: "Dest")],
subject: "-subject-",
),
Protocols::HEADERS => @headers,
Protocols::BODYTEXT => "Content of the mail",
},
)
super
end
def test_parse_rfc822_headers
h1 = Mail.parse_rfc822_headers("Key: Value\r\n")
assert_equal 1, h1.size
h1.each { |tuple| assert_equal 2, tuple.size }
assert_equal "Key", h1.first.first
assert_equal "Value", h1.first.last
h2 = Mail.parse_rfc822_headers("Key: Value\r\n\tPartTwo\r\n")
assert_equal 1, h2.size
h2.each { |tuple| assert_equal 2, tuple.size }
assert_equal "Value\r\n\tPartTwo", h2.first.last
end
def test_init_with_imap_mail
mail = Mail.new(imap_mail: @imap_mail)
assert_equal "Sender", mail.from_name
assert_equal "sender@local", mail.from
assert_equal "Dest", mail.to_name
assert_equal "dest@local", mail.to
assert_equal "SenderH <sender@head.local>", mail.header("From")
end
end
end

3
test/test_helper.rb Normal file
View File

@ -0,0 +1,3 @@
require "pry"
require "minitest"
require "mocha/minitest"