imap.rb/lib/imap_server/client_handler/search/builder.rb
2022-12-11 02:06:38 +01:00

257 lines
5.0 KiB
Ruby

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