Compare commits
3 Commits
3c15e243f7
...
b05502fedf
Author | SHA1 | Date |
---|---|---|
Arthur POULET | b05502fedf | |
Arthur POULET | 7573060bbd | |
Arthur POULET | 874660477e |
2
Gemfile
2
Gemfile
|
@ -28,3 +28,5 @@ gem "semver", "~> 1.0"
|
|||
group :develop do
|
||||
gem "pry", "~> 0.14.1"
|
||||
end
|
||||
|
||||
gem "mocha", "~> 2.0"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -16,3 +16,4 @@ DB_URL="sqlite://dev.db"
|
|||
PORT=10081
|
||||
DEBUG=false
|
||||
CPU_CYCLE=1
|
||||
LOG_FILE=/var/log/mailinglistrb.log
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,7 +31,8 @@ class Protocols::Mail
|
|||
@to = "#{envelope.to.first.mailbox}@#{envelope.to.first.host}"
|
||||
@subject = envelope.subject
|
||||
@body = body_text
|
||||
@headers = parse_rfc822_headers!(body_head)
|
||||
@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
|
||||
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
require "pry"
|
||||
require "minitest"
|
||||
require "mocha/minitest"
|
Loading…
Reference in New Issue