require "process" require "io" require "socket" require "./config" at_exit { GC.collect } module Taro enum MsgType : UInt8 MBOX_LIST GET_MBOX MAIL_LIST MARK_ALL_READ SEARCH_MAIL = 5 REFILE_MAIL = 7 TRASH_MAIL = 9 READ_MAIL = 11 COMPOSE_MAIL = 13 UPDATE_UI = 15 end MSG_SIZES = { MsgType::MBOX_LIST => 4096_u16, MsgType::MAIL_LIST => 32768_u16, } def u8arr_tou16(s : Slice(UInt8)) : UInt16 if s.size < 2 return 0_u16 else low : UInt16 = UInt16.new(s[0])*256 high : UInt16 = UInt16.new(s[1]) return low + high end end enum WinType LIST READER COMPOSE end class Mesg @type : MsgType @data : Slice(UInt8) def type @type end def data @data end def initialize(@type, @data) end end class ChildWindow include Taro @@msg : Channel(Mesg) = Channel(Mesg).new def self.msg @@msg end @lifetime : Channel(UInt8) @stdout_w : IO::FileDescriptor @stdout_r : IO::FileDescriptor @stdin_w : IO::FileDescriptor @stdin_r : IO::FileDescriptor @decoding : Bool = false @sizing : Bool = false @szshort : UInt16 = 0 def initialize(w : WinType = WinType::LIST, arg : String = "") @stdout_r, @stdout_w = IO.pipe @stdin_r, @stdin_w = IO.pipe @lifetime = Channel(UInt8).new case w when WinType::LIST then spawn do Process.run( command: "#{TARO_LIB}/#{UXN_EMU}", args: [ "taro-ls" ], chdir: TARO_LIB, input: @stdin_r, output: @stdout_w, error: Process::Redirect::Inherit) @lifetime.send(0) end spawn do loop do @@msg.send(read_msg) end end when WinType::READER then spawn do Process.run(command: READER_PROG + " " + arg, shell: true) @@msg.send(Mesg.new(MsgType::UPDATE_UI, Slice(UInt8).new(0))) end when WinType::COMPOSE then spawn do Process.run(command: COMPOSE_PROG, shell: true) @@msg.send(Mesg.new(MsgType::UPDATE_UI, Slice(UInt8).new(0))) end end end def lifetime @lifetime end def write_msg(msgtype : MsgType, data : Slice) msgsz = UInt16.new(data.size > MSG_SIZES[msgtype] ? MSG_SIZES[msgtype] : data.size) msgtype.value.to_io(@stdin_w, IO::ByteFormat::BigEndian) msgsz.to_io(@stdin_w, IO::ByteFormat::BigEndian) if msgsz == MSG_SIZES[msgtype] @stdin_w.write(data[0..msgsz - 2]) 10_u8.to_io(@stdin_w, IO::ByteFormat::BigEndian) elsif msgsz != 0 @stdin_w.write(data[0..msgsz - 1]) end end def read_msg msgType : MsgType = MsgType::MBOX_LIST data : Slice(UInt8) = Slice(UInt8).new(0) loop do if @stdout_r.closed? @decoding = false @sizing = false @szshort = 0 break end if !@decoding msgType = MsgType.new(@stdout_r.read_byte || 0_u8) @decoding = true @sizing = true elsif @sizing szslice = Slice(UInt8).new(2) @stdout_r.read_fully(szslice) @szshort = u8arr_tou16(szslice) @sizing = false if @szshort == 0 @decoding = false break end else slc = Slice(UInt8).new(@szshort) @stdout_r.read_fully(slc) data = slc @decoding = false @szshort = 0 break end end return Mesg.new(msgType, data) end end class CappedIO < IO::Memory def write(slice : Bytes) if slice.size >= MSG_SIZES[MsgType::MAIL_LIST] super(slice[0..MSG_SIZES[MsgType::MAIL_LIST] - 1]) else super(slice) end end end class MblazeProxy @@mailbox : String = "INBOX" @@search_regex : String = "" def self.mailbox @@mailbox end def self.search_regex @@search_regex end private def run_cmd(cmdtxt : String) : IO::Memory io = CappedIO.new Process.run(cmdtxt, shell: true, output: io) return io end def list_mail : IO::Memory if @@search_regex != "" return search_mail(@@search_regex, false, false) end cmd = " mbox=#{MBOX_ROOT}/#{@@mailbox} mdirs ${mbox} | xargs minc > /dev/null mlist ${mbox} | msort -dr | mseq -S | mscan" return run_cmd(cmd) end def list_mboxes : IO::Memory cmd = "echo 'INBOX' ; for x in $(mdirs -a #{MBOX_ROOT} | sort | grep -v INBOX); do echo ${x##{MBOX_ROOT}/}; done" return run_cmd(cmd) end def mark_all_read cmd = "mflag -S :" run_cmd(cmd) end def set_mbox(box : String) @@mailbox = box @@search_regex = "" end def trash_mail(range_start : UInt16, range_end : UInt16) cmd = "mrefile #{range_start}:#{range_end} #{MBOX_ROOT}/Trash" # destroy mail if we are trashing what's already in the trash! if @@mailbox == "Trash" cmd = "for x in $(mseq #{range_start}:#{range_end}); do rm $x; done" end run_cmd(cmd) end def refile_mail(range_start : UInt16, range_end : UInt16, to_mbox : String) canonical_mbox = run_cmd("readlink -f '#{MBOX_ROOT}/#{to_mbox.gsub("'", "\'")}'").to_s # if readlink is not available, do this naive trick with cd :shrug: if canonical_mbox.empty? canonical_mbox = run_cmd("cd #{MBOX_ROOT}/#{to_mbox}; pwd").to_s end if canonical_mbox.starts_with?(MBOX_ROOT) cmd = "mrefile #{range_start}:#{range_end} #{canonical_mbox.gsub(" ", "\ ").gsub("\"", "\\\"").gsub("'", "\'")}" run_cmd(cmd) end end def search_mail(query : String, body : Bool, case_sensitive : Bool) : IO::Memory query = query.gsub("'", "\'") @@search_regex = query cmd = "mlist #{MBOX_ROOT}/#{@@mailbox} | magrep #{case_sensitive ? "" : "-i"} #{body ? "/" : "*"}:'#{query}' | msort -dr | uniq | mseq -S | mscan" return run_cmd(cmd) end end class TaroCtl include Taro @mblaze : MblazeProxy @lsWin : ChildWindow @socket : UNIXServer def initialize @lsWin = ChildWindow.new @mblaze = MblazeProxy.new File.delete?("#{TARO_LIB}/taro.sock") @socket = UNIXServer.new("#{TARO_LIB}/taro.sock") list = @mblaze.list_mail mboxes = @mblaze.list_mboxes @lsWin.write_msg(MsgType::MBOX_LIST, mboxes.to_slice) @lsWin.write_msg(MsgType::MAIL_LIST, list.to_slice) end def run spawn do while client = @socket.accept? spawn do b = Slice(UInt8).new(1) client.read(b) ChildWindow.msg.send(Mesg.new(MsgType::UPDATE_UI, Slice(UInt8).new(0))) end end end loop do select when @lsWin.lifetime.receive? @socket.close exit when m = ChildWindow.msg.receive case m.type when MsgType::GET_MBOX then @mblaze.set_mbox(String.new(m.data)) @lsWin.write_msg(MsgType::MAIL_LIST, @mblaze.list_mail.to_slice) when MsgType::MARK_ALL_READ then @mblaze.mark_all_read @lsWin.write_msg(MsgType::MAIL_LIST, @mblaze.list_mail.to_slice) when MsgType::SEARCH_MAIL then search_results = @mblaze.search_mail(String.new(m.data), false, false) @lsWin.write_msg(MsgType::MAIL_LIST, search_results.to_slice) when MsgType::REFILE_MAIL then range_start = u8arr_tou16(m.data[0..1]) range_end = u8arr_tou16(m.data[2..3]) dest_mbox = String.new(m.data[4..]) @mblaze.refile_mail(range_start, range_end, dest_mbox) @lsWin.write_msg(MsgType::MAIL_LIST, @mblaze.list_mail.to_slice) when MsgType::TRASH_MAIL then range_start = u8arr_tou16(m.data[0..1]) range_end = u8arr_tou16(m.data[2..3]) @mblaze.trash_mail(range_start, range_end) @lsWin.write_msg(MsgType::MAIL_LIST, @mblaze.list_mail.to_slice) when MsgType::READ_MAIL then mmsg = u8arr_tou16(m.data[0..1]) ChildWindow.new(WinType::READER, mmsg.to_s) when MsgType::COMPOSE_MAIL then ChildWindow.new(WinType::COMPOSE) when MsgType::UPDATE_UI then @lsWin.write_msg(MsgType::MAIL_LIST, @mblaze.list_mail.to_slice) end end end end end end taro = Taro::TaroCtl.new taro.run