257 lines
5.0 KiB
Ruby
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
|