mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-04 21:40:33 +00:00
LibIMAP: Support for the FETCH command (*mostly)
This commit doesn't include support for FETCH BODY, because it's a bit big already. Rest assured, FETCH is the most complicated IMAP command, and we'll go back to simple boring ones shortly.
This commit is contained in:
parent
1e9dfdcdcc
commit
c152a9a594
Notes:
sideshowbarker
2024-07-18 12:24:56 +09:00
Author: https://github.com/X-yl Commit: https://github.com/SerenityOS/serenity/commit/c152a9a594d Pull-request: https://github.com/SerenityOS/serenity/pull/7735 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/MaxWipfli Reviewed-by: https://github.com/alimpfard
6 changed files with 509 additions and 0 deletions
|
@ -124,6 +124,10 @@ static ReadonlyBytes command_byte_buffer(CommandType command)
|
|||
return "LIST"sv.bytes();
|
||||
case CommandType::Select:
|
||||
return "SELECT"sv.bytes();
|
||||
case CommandType::Fetch:
|
||||
return "FETCH"sv.bytes();
|
||||
case CommandType::UIDFetch:
|
||||
return "UID FETCH"sv.bytes();
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
@ -177,6 +181,12 @@ RefPtr<Promise<Optional<SolidResponse>>> Client::list(StringView reference_name,
|
|||
return cast_promise<SolidResponse>(send_command(move(command)));
|
||||
}
|
||||
|
||||
RefPtr<Promise<Optional<SolidResponse>>> Client::fetch(FetchCommand request, bool uid)
|
||||
{
|
||||
auto command = Command { uid ? CommandType::UIDFetch : CommandType::Fetch, m_current_command, { request.serialize() } };
|
||||
return cast_promise<SolidResponse>(send_command(move(command)));
|
||||
}
|
||||
|
||||
RefPtr<Promise<Optional<Response>>> Client::send_simple_command(CommandType type)
|
||||
{
|
||||
auto command = Command { type, m_current_command, {} };
|
||||
|
|
|
@ -24,6 +24,7 @@ public:
|
|||
RefPtr<Promise<Optional<SolidResponse>>> login(StringView username, StringView password);
|
||||
RefPtr<Promise<Optional<SolidResponse>>> list(StringView reference_name, StringView mailbox_name);
|
||||
RefPtr<Promise<Optional<SolidResponse>>> select(StringView string);
|
||||
RefPtr<Promise<Optional<SolidResponse>>> fetch(FetchCommand request, bool uid);
|
||||
RefPtr<Promise<Optional<ContinueRequest>>> idle();
|
||||
RefPtr<Promise<Optional<SolidResponse>>> finish_idle();
|
||||
|
||||
|
|
|
@ -8,4 +8,105 @@
|
|||
|
||||
namespace IMAP {
|
||||
|
||||
String Sequence::serialize() const
|
||||
{
|
||||
if (start == end) {
|
||||
return AK::String::formatted("{}", start);
|
||||
} else {
|
||||
auto start_char = start != -1 ? String::formatted("{}", start) : "*";
|
||||
auto end_char = end != -1 ? String::formatted("{}", end) : "*";
|
||||
return String::formatted("{}:{}", start_char, end_char);
|
||||
}
|
||||
}
|
||||
|
||||
String FetchCommand::DataItem::Section::serialize() const
|
||||
{
|
||||
StringBuilder headers_builder;
|
||||
switch (type) {
|
||||
case SectionType::Header:
|
||||
return "HEADER";
|
||||
case SectionType::HeaderFields:
|
||||
case SectionType::HeaderFieldsNot: {
|
||||
if (type == SectionType::HeaderFields)
|
||||
headers_builder.append("HEADER.FIELDS (");
|
||||
else
|
||||
headers_builder.append("HEADERS.FIELDS.NOT (");
|
||||
|
||||
bool first = true;
|
||||
for (auto& field : headers.value()) {
|
||||
if (!first)
|
||||
headers_builder.append(" ");
|
||||
headers_builder.append(field);
|
||||
first = false;
|
||||
}
|
||||
headers_builder.append(")");
|
||||
return headers_builder.build();
|
||||
}
|
||||
case SectionType::Text:
|
||||
return "TEXT";
|
||||
case SectionType::Parts: {
|
||||
StringBuilder sb;
|
||||
bool first = true;
|
||||
for (int part : parts.value()) {
|
||||
if (!first)
|
||||
sb.append(".");
|
||||
sb.appendff("{}", part);
|
||||
first = false;
|
||||
}
|
||||
if (ends_with_mime) {
|
||||
sb.append(".MIME");
|
||||
}
|
||||
return sb.build();
|
||||
}
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
String FetchCommand::DataItem::serialize() const
|
||||
{
|
||||
switch (type) {
|
||||
case DataItemType::Envelope:
|
||||
return "ENVELOPE";
|
||||
case DataItemType::Flags:
|
||||
return "FLAGS";
|
||||
case DataItemType::InternalDate:
|
||||
return "INTERNALDATE";
|
||||
case DataItemType::UID:
|
||||
return "UID";
|
||||
case DataItemType::PeekBody:
|
||||
TODO();
|
||||
case DataItemType::BodySection:
|
||||
StringBuilder sb;
|
||||
sb.appendff("BODY[{}]", section.value().serialize());
|
||||
if (partial_fetch) {
|
||||
sb.appendff("<{}.{}>", start, octets);
|
||||
}
|
||||
|
||||
return sb.build();
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
String FetchCommand::serialize()
|
||||
{
|
||||
StringBuilder sequence_builder;
|
||||
bool first = true;
|
||||
for (auto& sequence : sequence_set) {
|
||||
if (!first) {
|
||||
sequence_builder.append(",");
|
||||
}
|
||||
sequence_builder.append(sequence.serialize());
|
||||
first = false;
|
||||
}
|
||||
|
||||
StringBuilder data_items_builder;
|
||||
first = true;
|
||||
for (auto& data_item : data_items) {
|
||||
if (!first) {
|
||||
data_items_builder.append(" ");
|
||||
}
|
||||
data_items_builder.append(data_item.serialize());
|
||||
first = false;
|
||||
}
|
||||
|
||||
return AK::String::formatted("{} ({})", sequence_builder.build(), data_items_builder.build());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,14 @@
|
|||
namespace IMAP {
|
||||
enum class CommandType {
|
||||
Capability,
|
||||
Fetch,
|
||||
Idle,
|
||||
List,
|
||||
Login,
|
||||
Logout,
|
||||
Noop,
|
||||
Select,
|
||||
UIDFetch,
|
||||
};
|
||||
|
||||
enum class MailboxFlag : unsigned {
|
||||
|
@ -53,11 +55,72 @@ enum class ResponseType : unsigned {
|
|||
UIDValidity = 1u << 6,
|
||||
Unseen = 1u << 7,
|
||||
PermanentFlags = 1u << 8,
|
||||
Fetch = 1u << 9,
|
||||
Bye = 1u << 13,
|
||||
};
|
||||
|
||||
enum class FetchResponseType : unsigned {
|
||||
Body = 1u << 1,
|
||||
UID = 1u << 2,
|
||||
InternalDate = 1u << 3,
|
||||
Envelope = 1u << 4,
|
||||
Flags = 1u << 5,
|
||||
};
|
||||
|
||||
class Parser;
|
||||
|
||||
// Set -1 for '*' i.e highest possible value.
|
||||
struct Sequence {
|
||||
int start;
|
||||
int end;
|
||||
|
||||
[[nodiscard]] String serialize() const;
|
||||
};
|
||||
|
||||
struct FetchCommand {
|
||||
enum class DataItemType {
|
||||
Envelope,
|
||||
Flags,
|
||||
InternalDate,
|
||||
UID,
|
||||
PeekBody,
|
||||
BodySection
|
||||
};
|
||||
|
||||
struct DataItem {
|
||||
enum class SectionType {
|
||||
Header,
|
||||
HeaderFields,
|
||||
HeaderFieldsNot,
|
||||
Text,
|
||||
Parts
|
||||
};
|
||||
struct Section {
|
||||
SectionType type;
|
||||
|
||||
Optional<Vector<int>> parts {};
|
||||
bool ends_with_mime {};
|
||||
|
||||
Optional<Vector<String>> headers {};
|
||||
|
||||
[[nodiscard]] String serialize() const;
|
||||
};
|
||||
|
||||
DataItemType type;
|
||||
|
||||
Optional<Section> section {};
|
||||
bool partial_fetch { false };
|
||||
int start { 0 };
|
||||
int octets { 0 };
|
||||
|
||||
[[nodiscard]] String serialize() const;
|
||||
};
|
||||
|
||||
Vector<Sequence> sequence_set;
|
||||
Vector<DataItem> data_items;
|
||||
|
||||
String serialize();
|
||||
};
|
||||
struct Command {
|
||||
public:
|
||||
CommandType type;
|
||||
|
@ -77,6 +140,115 @@ struct ListItem {
|
|||
String name;
|
||||
};
|
||||
|
||||
struct Address {
|
||||
Optional<String> name;
|
||||
Optional<String> source_route;
|
||||
Optional<String> mailbox;
|
||||
Optional<String> host;
|
||||
};
|
||||
struct Envelope {
|
||||
Optional<String> date; // Format of date not specified.
|
||||
Optional<String> subject;
|
||||
Optional<Vector<Address>> from;
|
||||
Optional<Vector<Address>> sender;
|
||||
Optional<Vector<Address>> reply_to;
|
||||
Optional<Vector<Address>> to;
|
||||
Optional<Vector<Address>> cc;
|
||||
Optional<Vector<Address>> bcc;
|
||||
Optional<String> in_reply_to;
|
||||
Optional<String> message_id;
|
||||
};
|
||||
|
||||
class FetchResponseData {
|
||||
public:
|
||||
[[nodiscard]] unsigned response_type() const
|
||||
{
|
||||
return m_response_type;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool contains_response_type(FetchResponseType response_type) const
|
||||
{
|
||||
return (static_cast<unsigned>(response_type) & m_response_type) != 0;
|
||||
}
|
||||
|
||||
void add_response_type(FetchResponseType type)
|
||||
{
|
||||
m_response_type |= static_cast<unsigned>(type);
|
||||
}
|
||||
|
||||
void add_body_data(FetchCommand::DataItem&& data_item, Optional<String>&& body)
|
||||
{
|
||||
add_response_type(FetchResponseType::Body);
|
||||
m_bodies.append({ move(data_item), move(body) });
|
||||
}
|
||||
|
||||
Vector<Tuple<FetchCommand::DataItem, Optional<String>>>& body_data()
|
||||
{
|
||||
VERIFY(contains_response_type(FetchResponseType::Body));
|
||||
return m_bodies;
|
||||
}
|
||||
|
||||
void set_uid(unsigned uid)
|
||||
{
|
||||
add_response_type(FetchResponseType::UID);
|
||||
m_uid = uid;
|
||||
}
|
||||
|
||||
[[nodiscard]] unsigned uid() const
|
||||
{
|
||||
VERIFY(contains_response_type(FetchResponseType::UID));
|
||||
return m_uid;
|
||||
}
|
||||
|
||||
void set_internal_date(Core::DateTime time)
|
||||
{
|
||||
add_response_type(FetchResponseType::InternalDate);
|
||||
m_internal_date = time;
|
||||
}
|
||||
|
||||
Core::DateTime& internal_date()
|
||||
{
|
||||
VERIFY(contains_response_type(FetchResponseType::InternalDate));
|
||||
return m_internal_date;
|
||||
}
|
||||
|
||||
void set_envelope(Envelope&& envelope)
|
||||
{
|
||||
add_response_type(FetchResponseType::Envelope);
|
||||
m_envelope = move(envelope);
|
||||
}
|
||||
|
||||
Envelope& envelope()
|
||||
{
|
||||
VERIFY(contains_response_type(FetchResponseType::Envelope));
|
||||
return m_envelope;
|
||||
}
|
||||
|
||||
void set_flags(Vector<String>&& flags)
|
||||
{
|
||||
add_response_type(FetchResponseType::Flags);
|
||||
m_flags = move(flags);
|
||||
}
|
||||
|
||||
Vector<String>& flags()
|
||||
{
|
||||
VERIFY(contains_response_type(FetchResponseType::Flags));
|
||||
return m_flags;
|
||||
}
|
||||
|
||||
FetchResponseData()
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
Vector<String> m_flags;
|
||||
Vector<Tuple<FetchCommand::DataItem, Optional<String>>> m_bodies;
|
||||
Core::DateTime m_internal_date;
|
||||
Envelope m_envelope;
|
||||
unsigned m_uid { 0 };
|
||||
unsigned m_response_type { 0 };
|
||||
};
|
||||
|
||||
class ResponseData {
|
||||
public:
|
||||
[[nodiscard]] unsigned response_type() const
|
||||
|
@ -212,6 +384,18 @@ public:
|
|||
return m_permanent_flags;
|
||||
}
|
||||
|
||||
void add_fetch_response(unsigned message, FetchResponseData&& data)
|
||||
{
|
||||
add_response_type(ResponseType::Fetch);
|
||||
m_fetch_responses.append(Tuple<unsigned, FetchResponseData> { move(message), move(data) });
|
||||
}
|
||||
|
||||
Vector<Tuple<unsigned, FetchResponseData>>& fetch_data()
|
||||
{
|
||||
VERIFY(contains_response_type(ResponseType::Fetch));
|
||||
return m_fetch_responses;
|
||||
}
|
||||
|
||||
void set_bye(Optional<String> message)
|
||||
{
|
||||
add_response_type(ResponseType::Bye);
|
||||
|
@ -238,6 +422,7 @@ private:
|
|||
unsigned m_unseen {};
|
||||
Vector<String> m_permanent_flags;
|
||||
Vector<String> m_flags;
|
||||
Vector<Tuple<unsigned, FetchResponseData>> m_fetch_responses;
|
||||
Optional<String> m_bye_message;
|
||||
};
|
||||
|
||||
|
|
|
@ -137,6 +137,9 @@ void Parser::parse_untagged()
|
|||
} else if (data_type.matches("RECENT")) {
|
||||
m_response.data().set_recent(number.value());
|
||||
consume("\r\n");
|
||||
} else if (data_type.matches("FETCH")) {
|
||||
auto fetch_response = parse_fetch_response();
|
||||
m_response.data().add_fetch_response(number.value(), move(fetch_response));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -214,6 +217,87 @@ Optional<StringView> Parser::parse_nstring()
|
|||
return { parse_string() };
|
||||
}
|
||||
|
||||
FetchResponseData Parser::parse_fetch_response()
|
||||
{
|
||||
consume(" (");
|
||||
auto fetch_response = FetchResponseData();
|
||||
|
||||
while (!try_consume(")")) {
|
||||
auto data_item = parse_fetch_data_item();
|
||||
switch (data_item.type) {
|
||||
case FetchCommand::DataItemType::Envelope: {
|
||||
consume(" (");
|
||||
auto date = parse_nstring();
|
||||
consume(" ");
|
||||
auto subject = parse_nstring();
|
||||
consume(" ");
|
||||
auto from = parse_address_list();
|
||||
consume(" ");
|
||||
auto sender = parse_address_list();
|
||||
consume(" ");
|
||||
auto reply_to = parse_address_list();
|
||||
consume(" ");
|
||||
auto to = parse_address_list();
|
||||
consume(" ");
|
||||
auto cc = parse_address_list();
|
||||
consume(" ");
|
||||
auto bcc = parse_address_list();
|
||||
consume(" ");
|
||||
auto in_reply_to = parse_nstring();
|
||||
consume(" ");
|
||||
auto message_id = parse_nstring();
|
||||
consume(")");
|
||||
Envelope envelope = {
|
||||
date.has_value() ? Optional<String>(date.value()) : Optional<String>(),
|
||||
subject.has_value() ? Optional<String>(subject.value()) : Optional<String>(),
|
||||
from,
|
||||
sender,
|
||||
reply_to,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
in_reply_to.has_value() ? Optional<String>(in_reply_to.value()) : Optional<String>(),
|
||||
message_id.has_value() ? Optional<String>(message_id.value()) : Optional<String>(),
|
||||
};
|
||||
fetch_response.set_envelope(move(envelope));
|
||||
break;
|
||||
}
|
||||
case FetchCommand::DataItemType::Flags: {
|
||||
consume(" ");
|
||||
auto flags = parse_list(+[](StringView x) { return String(x); });
|
||||
fetch_response.set_flags(move(flags));
|
||||
break;
|
||||
}
|
||||
case FetchCommand::DataItemType::InternalDate: {
|
||||
consume(" \"");
|
||||
auto date_view = parse_while([](u8 x) { return x != '"'; });
|
||||
consume("\"");
|
||||
auto date = Core::DateTime::parse("%d-%b-%Y %H:%M:%S %z", date_view).value();
|
||||
fetch_response.set_internal_date(date);
|
||||
break;
|
||||
}
|
||||
case FetchCommand::DataItemType::UID: {
|
||||
consume(" ");
|
||||
fetch_response.set_uid(parse_number());
|
||||
break;
|
||||
}
|
||||
case FetchCommand::DataItemType::PeekBody:
|
||||
// Spec doesn't allow for this in a response.
|
||||
m_parsing_failed = true;
|
||||
break;
|
||||
case FetchCommand::DataItemType::BodySection: {
|
||||
auto body = parse_nstring();
|
||||
fetch_response.add_body_data(move(data_item), body.has_value() ? body.release_value() : Optional<String>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!at_end() && m_buffer[position] != ')')
|
||||
consume(" ");
|
||||
}
|
||||
consume("\r\n");
|
||||
return fetch_response;
|
||||
}
|
||||
|
||||
StringView Parser::parse_literal_string()
|
||||
{
|
||||
consume("{");
|
||||
|
@ -351,4 +435,126 @@ StringView Parser::parse_while(Function<bool(u8)> should_consume)
|
|||
return StringView(m_buffer.data() + position - chars, chars);
|
||||
}
|
||||
|
||||
FetchCommand::DataItem Parser::parse_fetch_data_item()
|
||||
{
|
||||
auto msg_attr = parse_while([](u8 x) { return is_ascii_alpha(x) != 0; });
|
||||
if (msg_attr.equals_ignoring_case("BODY") && try_consume("[")) {
|
||||
auto data_item = FetchCommand::DataItem {
|
||||
.type = FetchCommand::DataItemType::BodySection,
|
||||
.section = { {} }
|
||||
};
|
||||
auto section_type = parse_while([](u8 x) { return x != ']' && x != ' '; });
|
||||
if (section_type.equals_ignoring_case("HEADER.FIELDS")) {
|
||||
data_item.section->type = FetchCommand::DataItem::SectionType::HeaderFields;
|
||||
data_item.section->headers = Vector<String>();
|
||||
consume(" ");
|
||||
auto headers = parse_list(+[](StringView x) { return x; });
|
||||
for (auto& header : headers) {
|
||||
data_item.section->headers->append(header);
|
||||
}
|
||||
consume("]");
|
||||
} else if (section_type.equals_ignoring_case("HEADER.FIELDS.NOT")) {
|
||||
data_item.section->type = FetchCommand::DataItem::SectionType::HeaderFieldsNot;
|
||||
data_item.section->headers = Vector<String>();
|
||||
consume(" (");
|
||||
auto headers = parse_list(+[](StringView x) { return x; });
|
||||
for (auto& header : headers) {
|
||||
data_item.section->headers->append(header);
|
||||
}
|
||||
consume("]");
|
||||
} else if (is_ascii_digit(section_type[0])) {
|
||||
data_item.section->type = FetchCommand::DataItem::SectionType::Parts;
|
||||
data_item.section->parts = Vector<int>();
|
||||
|
||||
while (!try_consume("]")) {
|
||||
auto num = parse_number();
|
||||
if (num != (unsigned)-1) {
|
||||
data_item.section->parts->append((int)num);
|
||||
continue;
|
||||
}
|
||||
auto atom = parse_atom();
|
||||
if (atom.equals_ignoring_case("MIME")) {
|
||||
data_item.section->ends_with_mime = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (section_type.equals_ignoring_case("TEXT")) {
|
||||
data_item.section->type = FetchCommand::DataItem::SectionType::Text;
|
||||
} else if (section_type.equals_ignoring_case("HEADER")) {
|
||||
data_item.section->type = FetchCommand::DataItem::SectionType::Header;
|
||||
} else {
|
||||
dbgln("Unmatched section type {}", section_type);
|
||||
m_parsing_failed = true;
|
||||
}
|
||||
if (try_consume("<")) {
|
||||
auto start = parse_number();
|
||||
data_item.partial_fetch = true;
|
||||
data_item.start = (int)start;
|
||||
consume(">");
|
||||
}
|
||||
try_consume(" ");
|
||||
return data_item;
|
||||
} else if (msg_attr.equals_ignoring_case("FLAGS")) {
|
||||
return FetchCommand::DataItem {
|
||||
.type = FetchCommand::DataItemType::Flags
|
||||
};
|
||||
} else if (msg_attr.equals_ignoring_case("UID")) {
|
||||
return FetchCommand::DataItem {
|
||||
.type = FetchCommand::DataItemType::UID
|
||||
};
|
||||
} else if (msg_attr.equals_ignoring_case("INTERNALDATE")) {
|
||||
return FetchCommand::DataItem {
|
||||
.type = FetchCommand::DataItemType::InternalDate
|
||||
};
|
||||
} else if (msg_attr.equals_ignoring_case("ENVELOPE")) {
|
||||
return FetchCommand::DataItem {
|
||||
.type = FetchCommand::DataItemType::Envelope
|
||||
};
|
||||
} else {
|
||||
dbgln("msg_attr not matched: {}", msg_attr);
|
||||
m_parsing_failed = true;
|
||||
return FetchCommand::DataItem {};
|
||||
}
|
||||
}
|
||||
Optional<Vector<Address>> Parser::parse_address_list()
|
||||
{
|
||||
if (try_consume("NIL"))
|
||||
return {};
|
||||
|
||||
auto addresses = Vector<Address>();
|
||||
consume("(");
|
||||
while (!try_consume(")")) {
|
||||
addresses.append(parse_address());
|
||||
if (!at_end() && m_buffer[position] != ')')
|
||||
consume(" ");
|
||||
}
|
||||
return { addresses };
|
||||
}
|
||||
|
||||
Address Parser::parse_address()
|
||||
{
|
||||
consume("(");
|
||||
auto address = Address();
|
||||
// I hate this so much. Why is there no Optional.map??
|
||||
auto name = parse_nstring();
|
||||
address.name = name.has_value() ? Optional<String>(name.value()) : Optional<String>();
|
||||
consume(" ");
|
||||
auto source_route = parse_nstring();
|
||||
address.source_route = source_route.has_value() ? Optional<String>(source_route.value()) : Optional<String>();
|
||||
consume(" ");
|
||||
auto mailbox = parse_nstring();
|
||||
address.mailbox = mailbox.has_value() ? Optional<String>(mailbox.value()) : Optional<String>();
|
||||
consume(" ");
|
||||
auto host = parse_nstring();
|
||||
address.host = host.has_value() ? Optional<String>(host.value()) : Optional<String>();
|
||||
consume(")");
|
||||
return address;
|
||||
}
|
||||
StringView Parser::parse_astring()
|
||||
{
|
||||
if (!at_end() && (m_buffer[position] == '{' || m_buffer[position] == '"'))
|
||||
return parse_string();
|
||||
else
|
||||
return parse_atom();
|
||||
}
|
||||
}
|
|
@ -60,7 +60,13 @@ private:
|
|||
|
||||
ListItem parse_list_item();
|
||||
|
||||
FetchCommand::DataItem parse_fetch_data_item();
|
||||
|
||||
FetchResponseData parse_fetch_response();
|
||||
|
||||
StringView parse_literal_string();
|
||||
Optional<Vector<Address>> parse_address_list();
|
||||
Address parse_address();
|
||||
StringView parse_astring();
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue