From 1ef6964dd792d9111c380e7f35f5b383a4c09118 Mon Sep 17 00:00:00 2001 From: 1resu <1resu@solidaris.me> Date: Sat, 18 Dec 2021 11:50:56 +0100 Subject: [PATCH] Add tasks files --- lib/tasks/bnn_sync.rake | 2 + lib/tasks/ftp_sync.rake | 7 +++ lib/tasks/import.rake | 31 +++++++++++ lib/tasks/mail.rake | 116 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 lib/tasks/bnn_sync.rake create mode 100644 lib/tasks/ftp_sync.rake create mode 100644 lib/tasks/import.rake create mode 100644 lib/tasks/mail.rake diff --git a/lib/tasks/bnn_sync.rake b/lib/tasks/bnn_sync.rake new file mode 100644 index 0000000..deaba27 --- /dev/null +++ b/lib/tasks/bnn_sync.rake @@ -0,0 +1,2 @@ +desc "Sync files via FTP -- legacy task name. Update articles." +task sync_bnn_files: :sync_ftp_files diff --git a/lib/tasks/ftp_sync.rake b/lib/tasks/ftp_sync.rake new file mode 100644 index 0000000..3c4102b --- /dev/null +++ b/lib/tasks/ftp_sync.rake @@ -0,0 +1,7 @@ +desc "Sync files via FTP. Update articles." +task sync_ftp_files: :environment do + Supplier.ftp_sync.all.each do |supplier| + puts "FTP-sync files for #{supplier.name}..." + supplier.sync_ftp_files + end +end diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake new file mode 100644 index 0000000..fd4cf64 --- /dev/null +++ b/lib/tasks/import.rake @@ -0,0 +1,31 @@ +# encoding:utf-8 +desc "Import articles from file (options: FILE=f, TYPE=t, SUPPLIER=id_or_name)" +task 'import:articles': :environment do |t, args| + filename = ENV['FILE'] + filename.present? or raise 'Please set FILE to the file to import' + + type = ENV['TYPE'] + types_avail = ArticleImport.file_formats.keys + types_avail.include?(type) or raise "Please set TYPE to one of: #{types_avail.join(', ')}" + + supplier = Supplier.where('id = ? OR name = ?', ENV['SUPPLIER'], ENV['SUPPLIER']).first + supplier.present? or raise 'Please set SUPPLIER to a supplier id or name' + + outlisted_counter, new_counter, updated_counter, invalid_articles = begin + Article.transaction do + supplier.update_articles_from_file(File.new(filename), type: type) + end + end + + # show result + # @todo remove code duplication with mail_sync + puts "* imported: #{new_counter} new, #{updated_counter} updated, #{outlisted_counter} outlisted, #{invalid_articles.size} invalid" + invalid_articles.each do |article| + puts "- invalid article '#{article.name}'" + article.errors.each do |attr, msg| + msg.split("\n").each do |l| + puts " ยท #{attr.blank? ? '' : "#{attr}: "} #{l}" + end + end + end +end diff --git a/lib/tasks/mail.rake b/lib/tasks/mail.rake new file mode 100644 index 0000000..be5de3d --- /dev/null +++ b/lib/tasks/mail.rake @@ -0,0 +1,116 @@ +# +# Rake tasks for receiving article updates by email. +# +# Mail setup is heavily inspired by Foodsoft's reply-by-mail feature for messages +# @see https://github.com/foodcoops/foodsoft/blob/master/lib/foodsoft_mail_receiver.rb +# +require "mail" +require "midi-smtp-server" + +class ReplyEmailSmtpServer < MidiSmtpServer::Smtpd + + def start + super + @from = nil + @supplier = nil + end + + private + + def on_mail_from_event(ctx, from) + @from = from + end + + def on_rcpt_to_event(ctx, rcpt_to) + recipient = rcpt_to.gsub(/\A\s*<\s*(.*)\s*>\s*\z/, '\1') + @supplier = find_supplier(recipient) + rcpt_to + rescue => error + logger.info("Can not accept mail for '#{rcpt_to}': #{error}") + raise MidiSmtpServer::Smtpd550Exception + end + + def on_message_data_event(ctx) + handle_mail(ctx[:message][:data]) + end + + def find_supplier(recipient) + m = /\A#{Regexp.escape(ENV['MAILER_PREFIX'])}(?\d+)\.(?\w+)(@(?.*))?/.match(recipient) + raise "recipient is missing or has an invalid format" if m.nil? + + supplier = Supplier.mail_sync.find_by_id(m[:supplier_id]) + raise "supplier id #{m[:supplier_id]} could not be found" if supplier.nil? + + hash = supplier.articles_mail_hash + hash.casecmp(m[:hash]) == 0 or raise "hash '#{m[:hash]}' does not match expectations for supplier '#{supplier.name}'" + + supplier + end + + def handle_mail(data) + message = Mail.read_from_string(data) + + # message checks + if @supplier.mail_from.present? + m, s = message.from, @supplier.mail_from + m.any? {|n| n.include?(s) } or raise "Expected to find '#{s}' in from address '#{m.join(', ')}' for supplier #{@supplier.name}" + end + if @supplier.mail_subject.present? + m, s = message.subject, @supplier.mail_subject + m.downcase.include?(s.downcase) or raise "Expected to find '#{s}' in subject '#{m}' for supplier #{@supplier.name}" + end + + # get attachment + filename = nil + message.attachments.each do |part| + # @todo perhaps get heuristic from article import filters? + if part.filename.match(/\.(xls|xlsx|ods|sxc|csv|tsv|xml)$/i) + FileUtils.mkdir_p(@supplier.mail_path) + filename = "#{message.date.strftime '%Y%m%d'}_#{part.filename.gsub(/[^-a-z0-9_\.]+/i, '_')}" + filename = @supplier.mail_path.join(filename) + File.open(filename, 'w+b') { |f| f.write part.body.decoded } + end + end + + raise "No spreadsheet attachment found" unless filename.present? + + # import! + outlisted_counter, new_counter, updated_counter, invalid_articles = + @supplier.update_articles_from_file(File.new(filename), type: @supplier.mail_type) + + msg = "Handled articles update email for #{@supplier.name}: " + msg += "#{new_counter} new, #{updated_counter} updated, #{outlisted_counter} outlisted, #{invalid_articles.size} invalid" + invalid_articles.map do |article| + msg += "\n* invalid article '#{article.name}'" + article.errors.each do |attr, errmsg| + errmsg.split("\n").each do |l| + msg += "\n - #{': ' unless attr.blank?}" + l + end + end + end + + logger.info(msg) + end +end + +namespace :mail do + desc "Parse incoming email on stdin (options: RECIPIENT=1.a1b2c3d3e5)" + task parse_reply_email: :environment do + hande_mail(ENV['RECIPIENT'], STDIN.read) + end + + desc "Start STMP server for incoming email (options: SMTP_SERVER_PORT=2525, SMTP_SERVER_HOST=127.0.0.1)" + task :smtp_server => :environment do + port = (ENV['SMTP_SERVER_PORT'] || 2525).to_i + host = ENV['SMTP_SERVER_HOST'] || '127.0.0.1' + rake_say "Started SMTP server for incoming email on #{host}:#{port}." + server = ReplyEmailSmtpServer.new(port, host) + server.start + server.join + end +end + +# Helper +def rake_say(message) + puts message unless Rake.application.options.silent +end