commit f0a75e9c7874167708380ded63640b0f3f0229ea
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Mon Mar 9 15:39:40 2009 +0100

    [evol] commands management rework #1: moved command parsing into the Mail class, so the CommandParser is now a CommandRunner, and does not have to understand mail parts at all

diff --git a/bin/postman b/bin/postman
index 3068a02..563345a 100755
--- a/bin/postman
+++ b/bin/postman
@@ -156,7 +156,7 @@ module CyborgHood
           end
 
           logger.debug "Message accepted, processing orders..."
-          result_list = CommandParser.run(order)
+          result_list = CommandRunner.run(order)
 
           # create transcript
           logger.debug "Preparing reply"
@@ -217,32 +217,32 @@ module CyborgHood
       end
     end
 
-    class CommandParser
+    class CommandRunner
       include GetText
 
       def self.run(order)
         result_list = []
-        order.commands.each do |cmdstr|
-          logger.info "Executing command: #{cmdstr}"
+        order.commands.each do |cmd|
+          logger.info "Executing command: #{cmd.cmdline}"
           begin
-            result = execute_cmd(order.user, cmdstr, order.references)
+            result = execute_cmd(order.user, cmd.cmdsplit, order.shared_params)
             if result.nil?
               result = OpenStruct.new
-              result.cmd = cmdstr
+              result.cmd = cmd.cmdline
               result.ok = false
               result.message = _("Command not recognized.")
               result.refs = nil
             end
           rescue CyberError => e
             result = OpenStruct.new
-            result.cmd = cmdstr
+            result.cmd = cmd.cmdline
             result.ok = false
             result.message = e.message.capitalize + "."
             result.refs = nil
           rescue
             logger.warn "Command crashed: " + $!
             result = OpenStruct.new
-            result.cmd = cmdstr
+            result.cmd = cmd.cmdline
             result.ok = false
             result.message = _("Internal error. Administrator is warned.")
             result.refs = nil
@@ -257,8 +257,7 @@ module CyborgHood
 
       private
 
-      def self.execute_cmd(user, cmdstr, refs)
-        cmdline = Shellwords.shellwords(cmdstr)
+      def self.execute_cmd(user, cmdline, shared_params)
         subsys = cmdline.shift
 
         result = OpenStruct.new
@@ -316,21 +315,19 @@ module CyborgHood
               srv_dns = CyborgHood::Services::DNS.new(zone)
 
               return if cmdline.empty?
-              content_ref = cmdline.shift.downcase
-              return unless content_ref =~ /^@(\d+)$/
-              part_ref = $1.to_i
-              unless (1..refs.size).include? part_ref
+              content_ref = cmdline.shift
+              if content_ref.nil?
                 result.message = _("Attachment number not found.")
                 return result
               end
-              part = refs[part_ref]
-              unless part.content_type == "text/plain"
+              part = shared_params[content_ref]
+              unless part.type == "text/plain"
                 result.message = _("Attachment has wrong content-type.")
                 return result
               end
 
               f = Tempfile.new(zone)
-              f.write(part.body)
+              f.write(part.content)
               f.close
               logger.debug "Created temporary zone file '#{f.path}'"
 
diff --git a/lib/cyborghood/mail.rb b/lib/cyborghood/mail.rb
index dbe7a1e..e548c07 100644
--- a/lib/cyborghood/mail.rb
+++ b/lib/cyborghood/mail.rb
@@ -38,8 +38,34 @@ require 'digest/md5'
 # This class handles RFC3156 signed messages, validates them, and extract orders properly.
 # Encrypted content are not implemented yet.
 module CyborgHood
+  class Command
+    attr_reader :cmdline, :cmdsplit
+
+    def initialize(cmdline, cmdsplit)
+      @cmdline = cmdline
+      @cmdsplit = cmdsplit
+    end
+  end
+
+  class SharedParameter
+    attr_reader :type, :content
+
+    def initialize(content, type = nil)
+      @content = content
+      @type = type
+    end
+  end
+
+  class ParameterReference
+    attr_reader :reference
+
+    def initialize(reference)
+      @reference = reference
+    end
+  end
+
   class Order
-    attr_accessor :ok, :message, :system_message, :user, :commands, :references
+    attr_accessor :ok, :message, :system_message, :user, :commands, :shared_parameters
     attr_writer = :warn_sender
 
     def initialize(ok, message = nil, system_message = nil)
@@ -167,15 +193,22 @@ module CyborgHood
 
     def parse_plain
       command_txt = nil
-      refs = nil
+      shared_params = nil
       if multipart?
         if parts[0].content_type == "text/plain"
           command_txt = self.parts[0].body
-          refs = self.parts.collect{|p| p.dup }
+          shared_params = {}
+          i = 1
+          self.parts.each do |p|
+            shared_params[i] = SharedParameter.new(p.body, p.content_type)
+            filename = p.header['content-type'].params('filename')
+            shared_params[filename] = ParameterReference.new(i) if filename
+            i += 1
+          end
         end
       else
         command_txt = self.body if self.content_type == "text/plain"
-        refs = []
+        shared_params = {}
       end
       unless command_txt
         order = Order.new(false, N_("Mail does not contain a proper text part for commands."))
@@ -192,19 +225,46 @@ module CyborgHood
         # stop processing when detecting message signature
         break if line == "-- "
 
-        commands << sline
+        used_refs = []
+        cmd_parts = sline.shellsplit.collect do |word|
+          if =~ /^@([a-zA-Z0-9._-]+)$/
+            ref = $1
+            d_ref, d_param = dereference_param(shared_params, param)
+            # TODO: should add error message for attachment not found in the Command
+            used_refs << d_ref
+            d_param
+          else
+            word
+          end
+        end
+
+        commands << Command.new(sline, cmd_parts)
       end
 
+      shared_params.delete_if{|ref, param| not used_refs.include?(ref) }
+
       logger.debug "Mail OK"
       mark_processed(self.signature_timestamp)
 
       order = Order.new(true)
       order.user = self.user
       order.commands = commands
-      order.references = refs
+      order.references = shared_params
       order
     end
 
+    def dereference_param(shared_params, param)
+      if param.is_a? SharedParameter
+        [ref, ParameterReference(ref)]
+      elsif param.is_a? ParameterReference
+        d_ref = param.reference
+        d_param = shared_params[d_ref]
+        return dereference_param(shared_params, d_param)
+      else
+        nil
+      end
+    end
+
     def parse_signed
       sigs_check = verify_pgp_signature()
       return Order.new(false, N_("Mail not formatted correctly (signed part).")) if sigs_check.nil? or sigs_check.size != 1
