Sequel.extension :escaped_like class ImapServer::ClientHandler::Search::Builder def initialize(mailbox) @sql = Mail.where(mailbox: mailbox) @where_parts = [] @fulltext = [] end def conclude! @sql = @sql.select(:id, :uid) if @fulltext.empty? if !@where_parts.empty? @where_parts.each do |part| @sql = @sql.where(part) end end $logger.debug @sql.sql sql_results = @sql.all if !@fulltext.empty? $logger.debug "apply some fulltext search" sql_results.filter! do |mail| @fulltext.all? do |fulltext| fulltext.call(mail) end end end sql_results end # @return the amount of token to skip after the current one def apply_token!(token, mutable_args) token.downcase! if SEARCH_TOKENS.key?(token) args_tokens = mutable_args.shift(SEARCH_TOKENS[token]) args_tokens.empty? ? send(token) : send(token, *args_tokens) elsif MODIFIER_TOKENS.key?(token) # just ignore them for now args_tokens = mutable_args.shift(MODIFIER_TOKENS[token]) args_tokens.empty? ? send(token) : send(token, *args_tokens) else raise "Invalid search token \"#{token}\"" end end MODIFIER_TOKENS = { "not" => 0, }.freeze SEARCH_TOKENS = { # "or" => 2, "uid" => 1, "seen" => 0, "unseen" => 0, "answered" => 0, "unanswered" => 0, "deleted" => 0, "undeleted" => 0, "draft" => 0, "undraft" => 0, "flagged" => 0, "unflagged" => 0, "recent" => 0, "new" => 0, "all" => 0, "old" => 0, # "keyword" => 0, # "unkeyword" => 0, "header" => 2, "bcc" => 1, # "before" => 1, "body" => 1, "cc" => 1, "from" => 1, "larger" => 1, # "on" => 1, # "sentbefore" => 1, # "senton" => 1, # "sentsince" => 1, # "since" => 1, "smaller" => 1, "subject" => 1, "text" => 1, "to" => 1, }.freeze module Modifier def not @not = true end end include Modifier module Sql def add_where!(*list, **options) options = if @not Sequel.~(*list, **options) else Sequel.&(*list, **options) end @where_parts << options @not = false end end module Uid include Sql def uid(seq_arg) if seq_arg.include?(":") bounds = seq_arg.split(":").map(&:to_i).sort range = (bounds[0])..(bounds[1]) add_where!(uid: range) elsif seq_arg.include?(",") list = seq_arg.split(",").map(&:to_i) add_where!(uid: list) elsif seq_arg.match(/^\d+$/) add_where!(uid: seq_arg.to_i) else raise "Invalid UID sequence, invalid arg #{seq_arg}" end end end include Uid module Headers include Sql def header(key, value) # add_where! Sequel[:raw].escaped_ilike("?:%?%", key, value) add_where!( Sequel.ilike( :headers, /(^|\r\n)#{Regexp.escape(key)}: [^\n]*#{Regexp.escape(value)}[^\n]*(\r\n|$)/i, ) ) end def bcc(value) header("Bcc", value) end def cc(value) header("Cc", value) end def from(value) header("From", value) end def subject(value) header("Subject", value) end def to(value) header("To", value) end end include Headers module Flags include Sql def seen add_where!(flag_seen: true) end def unseen add_where!(flag_seen: false) end def unanswered add_where!(flag_answered: false) end def answered add_where!(flag_answered: true) end def undeleted add_where!(flag_deleted: false) end def deleted add_where!(flag_deleted: true) end def undeleted add_where!(flag_deleted: false) end def deleted add_where!(flag_deleted: true) end def recent add_where!(flag_recent: true) end def old add_where!(flag_recent: false) end def new add_where!(flag_recent: true, flag_seen: false) end end include Flags module Raw include Sql def text(value) add_where! Sequel[:raw].escaped_ilike("%?%", value) end def body(value) add_where! Sequel.ilike( :raw, /\r\n\r\n.*#{Regexp.escape(value)}/i, ) end # def header(key, value) # @fulltext << proc { |mail| mail.headers.match?(/^#{Regexp.escape(key)}: [^\n]*#{Regexp.escape(value)}[^\n]*\r?$/i) } # end def larger(value) @fulltext << proc { _1.raw.size >= value.to_i } end # TODO: parse dates, don't keep time def on(value) header("Date", value) end def before(value); end def sentbefore(value); end def senton(value); end def sentsince(value); end def since(value); end def larger(value) @fulltext << proc { _1.raw.size <= value.to_i } end end include Raw end