mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 20:07:36 +00:00
LibIMAP: Support for the SEARCH command
This commit is contained in:
parent
318709c8ca
commit
a6339297ec
5 changed files with 188 additions and 0 deletions
|
@ -126,8 +126,12 @@ static ReadonlyBytes command_byte_buffer(CommandType command)
|
||||||
return "SELECT"sv.bytes();
|
return "SELECT"sv.bytes();
|
||||||
case CommandType::Fetch:
|
case CommandType::Fetch:
|
||||||
return "FETCH"sv.bytes();
|
return "FETCH"sv.bytes();
|
||||||
|
case CommandType::Search:
|
||||||
|
return "SEARCH"sv.bytes();
|
||||||
case CommandType::UIDFetch:
|
case CommandType::UIDFetch:
|
||||||
return "UID FETCH"sv.bytes();
|
return "UID FETCH"sv.bytes();
|
||||||
|
case CommandType::UIDSearch:
|
||||||
|
return "UID SEARCH"sv.bytes();
|
||||||
}
|
}
|
||||||
VERIFY_NOT_REACHED();
|
VERIFY_NOT_REACHED();
|
||||||
}
|
}
|
||||||
|
@ -244,6 +248,20 @@ void Client::send_next_command()
|
||||||
send_raw(buffer);
|
send_raw(buffer);
|
||||||
m_expecting_response = true;
|
m_expecting_response = true;
|
||||||
}
|
}
|
||||||
|
RefPtr<Promise<Optional<SolidResponse>>> Client::search(Optional<String> charset, Vector<SearchKey>&& keys, bool uid)
|
||||||
|
{
|
||||||
|
Vector<String> args;
|
||||||
|
if (charset.has_value()) {
|
||||||
|
args.append("CHARSET ");
|
||||||
|
args.append(charset.value());
|
||||||
|
}
|
||||||
|
for (const auto& item : keys) {
|
||||||
|
args.append(item.serialize());
|
||||||
|
}
|
||||||
|
auto command = Command { uid ? CommandType::UIDSearch : CommandType::Search, m_current_command, args };
|
||||||
|
return cast_promise<SolidResponse>(send_command(move(command)));
|
||||||
|
}
|
||||||
|
|
||||||
RefPtr<Promise<Optional<ContinueRequest>>> Client::idle()
|
RefPtr<Promise<Optional<ContinueRequest>>> Client::idle()
|
||||||
{
|
{
|
||||||
auto promise = send_simple_command(CommandType::Idle);
|
auto promise = send_simple_command(CommandType::Idle);
|
||||||
|
|
|
@ -24,6 +24,7 @@ public:
|
||||||
RefPtr<Promise<Optional<SolidResponse>>> login(StringView username, StringView password);
|
RefPtr<Promise<Optional<SolidResponse>>> login(StringView username, StringView password);
|
||||||
RefPtr<Promise<Optional<SolidResponse>>> list(StringView reference_name, StringView mailbox_name);
|
RefPtr<Promise<Optional<SolidResponse>>> list(StringView reference_name, StringView mailbox_name);
|
||||||
RefPtr<Promise<Optional<SolidResponse>>> select(StringView string);
|
RefPtr<Promise<Optional<SolidResponse>>> select(StringView string);
|
||||||
|
RefPtr<Promise<Optional<SolidResponse>>> search(Optional<String> charset, Vector<SearchKey>&& search_keys, bool uid);
|
||||||
RefPtr<Promise<Optional<SolidResponse>>> fetch(FetchCommand request, bool uid);
|
RefPtr<Promise<Optional<SolidResponse>>> fetch(FetchCommand request, bool uid);
|
||||||
RefPtr<Promise<Optional<ContinueRequest>>> idle();
|
RefPtr<Promise<Optional<ContinueRequest>>> idle();
|
||||||
RefPtr<Promise<Optional<SolidResponse>>> finish_idle();
|
RefPtr<Promise<Optional<SolidResponse>>> finish_idle();
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <AK/CharacterTypes.h>
|
||||||
#include <LibIMAP/Objects.h>
|
#include <LibIMAP/Objects.h>
|
||||||
|
|
||||||
namespace IMAP {
|
namespace IMAP {
|
||||||
|
@ -112,4 +113,77 @@ String FetchCommand::serialize()
|
||||||
|
|
||||||
return AK::String::formatted("{} ({})", sequence_builder.build(), data_items_builder.build());
|
return AK::String::formatted("{} ({})", sequence_builder.build(), data_items_builder.build());
|
||||||
}
|
}
|
||||||
|
String serialize_astring(StringView string)
|
||||||
|
{
|
||||||
|
// Try to send an atom
|
||||||
|
auto is_non_atom_char = [](char x) {
|
||||||
|
auto non_atom_chars = { '(', ')', '{', ' ', '%', '*', '"', '\\', ']' };
|
||||||
|
return AK::find(non_atom_chars.begin(), non_atom_chars.end(), x) != non_atom_chars.end();
|
||||||
|
};
|
||||||
|
auto is_atom = all_of(string.begin(), string.end(), [&](auto ch) { return is_ascii_control(ch) && !is_non_atom_char(ch); });
|
||||||
|
if (is_atom) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to quote
|
||||||
|
auto can_be_quoted = !(string.contains('\n') || string.contains('\r'));
|
||||||
|
if (can_be_quoted) {
|
||||||
|
auto escaped_str = string.to_string();
|
||||||
|
escaped_str.replace("\\", "\\\\");
|
||||||
|
escaped_str.replace("\"", "\\\"");
|
||||||
|
return String::formatted("\"{}\"", escaped_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just send a literal
|
||||||
|
return String::formatted("{{{}}}\r\n{}", string.length(), string);
|
||||||
|
}
|
||||||
|
String SearchKey::serialize() const
|
||||||
|
{
|
||||||
|
return data.visit(
|
||||||
|
[&](Empty const&) { VERIFY_NOT_REACHED(); return String("The compiler complains if you remove this."); },
|
||||||
|
[&](All const&) { return String("ALL"); },
|
||||||
|
[&](Answered const&) { return String("ANSWERED"); },
|
||||||
|
[&](Bcc const& x) { return String::formatted("BCC {}", serialize_astring(x.bcc)); },
|
||||||
|
[&](Cc const& x) { return String::formatted("CC {}", serialize_astring(x.cc)); },
|
||||||
|
[&](Deleted const&) { return String("DELETED"); },
|
||||||
|
[&](Draft const&) { return String("DRAFT"); },
|
||||||
|
[&](From const& x) { return String::formatted("FROM {}", serialize_astring(x.from)); },
|
||||||
|
[&](Header const& x) { return String::formatted("HEADER {} {}", serialize_astring(x.header), serialize_astring(x.value)); },
|
||||||
|
[&](Keyword const& x) { return String::formatted("KEYWORD {}", x.keyword); },
|
||||||
|
[&](Larger const& x) { return String::formatted("LARGER {}", x.number); },
|
||||||
|
[&](New const&) { return String("NEW"); },
|
||||||
|
[&](Not const& x) { return String::formatted("NOT {}", x.operand->serialize()); },
|
||||||
|
[&](Old const&) { return String("OLD"); },
|
||||||
|
[&](On const& x) { return String::formatted("ON {}", x.date.to_string("%d-%b-%Y")); },
|
||||||
|
[&](Or const& x) { return String::formatted("OR {} {}", x.lhs->serialize(), x.rhs->serialize()); },
|
||||||
|
[&](Recent const&) { return String("RECENT"); },
|
||||||
|
[&](SearchKeys const& x) {
|
||||||
|
StringBuilder sb;
|
||||||
|
sb.append("(");
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& item : x.keys) {
|
||||||
|
if (!first)
|
||||||
|
sb.append(", ");
|
||||||
|
sb.append(item->serialize());
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
return sb.build();
|
||||||
|
},
|
||||||
|
[&](Seen const&) { return String("SEEN"); },
|
||||||
|
[&](SentBefore const& x) { return String::formatted("SENTBEFORE {}", x.date.to_string("%d-%b-%Y")); },
|
||||||
|
[&](SentOn const& x) { return String::formatted("SENTON {}", x.date.to_string("%d-%b-%Y")); },
|
||||||
|
[&](SentSince const& x) { return String::formatted("SENTSINCE {}", x.date.to_string("%d-%b-%Y")); },
|
||||||
|
[&](SequenceSet const& x) { return x.sequence.serialize(); },
|
||||||
|
[&](Since const& x) { return String::formatted("SINCE {}", x.date.to_string("%d-%b-%Y")); },
|
||||||
|
[&](Smaller const& x) { return String::formatted("SMALLER {}", x.number); },
|
||||||
|
[&](Subject const& x) { return String::formatted("SUBJECT {}", serialize_astring(x.subject)); },
|
||||||
|
[&](Text const& x) { return String::formatted("TEXT {}", serialize_astring(x.text)); },
|
||||||
|
[&](To const& x) { return String::formatted("TO {}", serialize_astring(x.to)); },
|
||||||
|
[&](UID const& x) { return String::formatted("UID {}", x.uid); },
|
||||||
|
[&](Unanswered const&) { return String("UNANSWERED"); },
|
||||||
|
[&](Undeleted const&) { return String("UNDELETED"); },
|
||||||
|
[&](Undraft const&) { return String("UNDRAFT"); },
|
||||||
|
[&](Unkeyword const& x) { return String::formatted("UNKEYWORD {}", serialize_astring(x.flag_keyword)); },
|
||||||
|
[&](Unseen const&) { return String("UNSEEN"); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,10 @@ enum class CommandType {
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
Noop,
|
Noop,
|
||||||
|
Search,
|
||||||
Select,
|
Select,
|
||||||
UIDFetch,
|
UIDFetch,
|
||||||
|
UIDSearch,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class MailboxFlag : unsigned {
|
enum class MailboxFlag : unsigned {
|
||||||
|
@ -56,6 +58,7 @@ enum class ResponseType : unsigned {
|
||||||
Unseen = 1u << 7,
|
Unseen = 1u << 7,
|
||||||
PermanentFlags = 1u << 8,
|
PermanentFlags = 1u << 8,
|
||||||
Fetch = 1u << 9,
|
Fetch = 1u << 9,
|
||||||
|
Search = 1u << 10,
|
||||||
Bye = 1u << 13,
|
Bye = 1u << 13,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -319,6 +322,77 @@ private:
|
||||||
BodyStructure m_body_structure;
|
BodyStructure m_body_structure;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String serialize_astring(StringView string);
|
||||||
|
|
||||||
|
struct SearchKey {
|
||||||
|
public:
|
||||||
|
// clang-format off
|
||||||
|
struct All { };
|
||||||
|
struct Answered { };
|
||||||
|
struct Bcc { String bcc; };
|
||||||
|
struct Cc { String cc; };
|
||||||
|
struct Deleted { };
|
||||||
|
struct Draft { };
|
||||||
|
struct From { String from; };
|
||||||
|
struct Header { String header; String value; };
|
||||||
|
struct Keyword { String keyword; };
|
||||||
|
struct Larger { unsigned number; };
|
||||||
|
struct New { };
|
||||||
|
struct Not { OwnPtr<SearchKey> operand; };
|
||||||
|
struct Old { };
|
||||||
|
struct On { Core::DateTime date; };
|
||||||
|
struct Or { OwnPtr<SearchKey> lhs; OwnPtr<SearchKey> rhs; };
|
||||||
|
struct Recent { };
|
||||||
|
struct SearchKeys { Vector<OwnPtr<SearchKey>> keys; };
|
||||||
|
struct Seen { };
|
||||||
|
struct SentBefore { Core::DateTime date; };
|
||||||
|
struct SentOn { Core::DateTime date; };
|
||||||
|
struct SentSince { Core::DateTime date; };
|
||||||
|
struct SequenceSet { Sequence sequence; };
|
||||||
|
struct Since { Core::DateTime date; };
|
||||||
|
struct Smaller { unsigned number; };
|
||||||
|
struct Subject { String subject; };
|
||||||
|
struct Text { String text; };
|
||||||
|
struct To { String to; };
|
||||||
|
struct UID { unsigned uid; };
|
||||||
|
struct Unanswered { };
|
||||||
|
struct Undeleted { };
|
||||||
|
struct Undraft { };
|
||||||
|
struct Unkeyword { String flag_keyword; };
|
||||||
|
struct Unseen { };
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
Variant<Empty, All, Answered, Bcc, Cc, Deleted, Draft, From, Header, Keyword,
|
||||||
|
Larger, New, Not, Old, On, Or, Recent, SearchKeys, Seen, SentBefore, SentOn,
|
||||||
|
SentSince, SequenceSet, Since, Smaller, Subject, Text, To, UID, Unanswered,
|
||||||
|
Undeleted, Undraft, Unkeyword, Unseen>
|
||||||
|
data;
|
||||||
|
|
||||||
|
SearchKey(SearchKey&& other) noexcept
|
||||||
|
: data(move(other.data))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
explicit SearchKey(T&& t)
|
||||||
|
: data(std::forward<T>(t))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchKey& operator=(SearchKey&& other) noexcept
|
||||||
|
{
|
||||||
|
if (this == &other) {
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->data = move(other.data);
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] String serialize() const;
|
||||||
|
};
|
||||||
|
|
||||||
class ResponseData {
|
class ResponseData {
|
||||||
public:
|
public:
|
||||||
[[nodiscard]] unsigned response_type() const
|
[[nodiscard]] unsigned response_type() const
|
||||||
|
@ -466,6 +540,18 @@ public:
|
||||||
return m_fetch_responses;
|
return m_fetch_responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_search_results(Vector<unsigned>&& results)
|
||||||
|
{
|
||||||
|
add_response_type(ResponseType::Search);
|
||||||
|
m_search_results = move(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<unsigned>& search_results()
|
||||||
|
{
|
||||||
|
VERIFY(contains_response_type(ResponseType::Search));
|
||||||
|
return m_search_results;
|
||||||
|
}
|
||||||
|
|
||||||
void set_bye(Optional<String> message)
|
void set_bye(Optional<String> message)
|
||||||
{
|
{
|
||||||
add_response_type(ResponseType::Bye);
|
add_response_type(ResponseType::Bye);
|
||||||
|
@ -493,6 +579,7 @@ private:
|
||||||
Vector<String> m_permanent_flags;
|
Vector<String> m_permanent_flags;
|
||||||
Vector<String> m_flags;
|
Vector<String> m_flags;
|
||||||
Vector<Tuple<unsigned, FetchResponseData>> m_fetch_responses;
|
Vector<Tuple<unsigned, FetchResponseData>> m_fetch_responses;
|
||||||
|
Vector<unsigned> m_search_results;
|
||||||
Optional<String> m_bye_message;
|
Optional<String> m_bye_message;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,14 @@ void Parser::parse_untagged()
|
||||||
parse_while([](u8 x) { return x != '\r'; });
|
parse_while([](u8 x) { return x != '\r'; });
|
||||||
consume("\r\n");
|
consume("\r\n");
|
||||||
}
|
}
|
||||||
|
} else if (try_consume("SEARCH")) {
|
||||||
|
Vector<unsigned> ids;
|
||||||
|
while (!try_consume("\r\n")) {
|
||||||
|
consume(" ");
|
||||||
|
auto id = parse_number();
|
||||||
|
ids.append(id);
|
||||||
|
}
|
||||||
|
m_response.data().set_search_results(move(ids));
|
||||||
} else if (try_consume("BYE")) {
|
} else if (try_consume("BYE")) {
|
||||||
auto message = parse_while([](u8 x) { return x != '\r'; });
|
auto message = parse_while([](u8 x) { return x != '\r'; });
|
||||||
consume("\r\n");
|
consume("\r\n");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue