System/Libraries/Imap: Add initial support for IMAP protocol

This commit is contained in:
Alec Murphy
2025-09-24 15:56:44 -04:00
parent 52b7136e01
commit df4a9a484a

517
System/Libraries/Imap.HC Normal file
View File

@@ -0,0 +1,517 @@
class ImapBuffer {
U8* rx;
U8* tx;
};
class ImapClient {
CTask* mem_task;
TlsSocket* s;
JsonObject* o;
ImapBuffer buf;
U8 response_text[1024];
U8 selected[256];
I64 state;
I64 tag;
U0 (*close)();
U0 (*connect)(U8* host, I64 port);
U0 (*fetch)(I64 uid);
JsonArray* (*fetch_array)(U8* sequence_set, U8* data_items);
U0 (*select)(U8* mailbox);
U0 (*login)(U8* userid, U8* password);
U0 (*logout)();
};
#define IMAP_BUFFER_SIZE 65536
#define IMAP_LINE_EQ 1
#define IMAP_LINE_IQ 2
#define IMAP_STATE_NOT_CONNECTED 0
#define IMAP_STATE_NOT_AUTHENTICATED 1
#define IMAP_STATE_AUTHENTICATED 2
#define IMAP_STATE_SELECTED 3
#define IMAP_STATE_LOGOUT 128
#define IMAP_STATE_ERROR 255
#define IMAP_WRAPPER_MAGIC_NUMBER 0xCAFEFACADEDEFACE
JsonArray* @imap_json_array_from_param_list(ImapClient* c, U8* line)
{
I64 i = 0;
I64 state = 0;
JsonArray* a = NULL;
U8* out = CAlloc(StrLen(line) * 2, c->mem_task);
while (line[i]) {
switch (state) {
case 0:
if (line[i] < ' ') {
++i;
goto @imap_convert_process_next_ch;
}
if (line[i] == ' ') {
String.Append(out, ",");
++i;
goto @imap_convert_process_next_ch;
}
if (line[i] == '(') {
String.Append(out, "[");
++i;
goto @imap_convert_process_next_ch;
}
if (line[i] == ')') {
String.Append(out, "]");
++i;
goto @imap_convert_process_next_ch;
}
if (line[i] == '"') {
state = IMAP_LINE_EQ;
String.Append(out, "%c", line[i]);
++i;
goto @imap_convert_process_next_ch;
}
state = IMAP_LINE_IQ;
String.Append(out, "\"");
String.Append(out, "%c", line[i]);
++i;
break;
case IMAP_LINE_EQ:
if (line[i] == '"') {
state = 0;
}
String.Append(out, "%c", line[i]);
++i;
break;
case IMAP_LINE_IQ:
if (line[i] == ')' || line[i] == ' ') {
state = 0;
String.Append(out, "\"");
goto @imap_convert_process_next_ch;
}
String.Append(out, "%c", line[i]);
++i;
break;
default:
// We shouldn't be here! Return empty Array
StrCpy(out, "[]");
i = StrLen(line);
break;
}
@imap_convert_process_next_ch:
}
a = Json.Parse(out, c->mem_task);
Free(out);
return a;
}
U8** @imap_lineate_buffer(ImapClient* c, U8* line_cnt_out)
{
if (!c || !line_cnt_out)
return NULL;
I64 line_cnt = StrOcc(c->buf.rx, '\n');
*line_cnt_out = line_cnt;
if (!line_cnt)
return NULL;
I64 i = 0;
U8* p = c->buf.rx;
U8** lines = CAlloc(sizeof(U8*) * line_cnt, c->mem_task);
lines[i++] = p++;
while (*p) {
switch (*p) {
case '\r':
*p = NULL;
p += 2;
lines[i++] = p++;
break;
default:
++p;
break;
}
}
return lines;
}
U0 @imap_init_buffers(ImapClient* c)
{
c->buf.rx = CAlloc(IMAP_BUFFER_SIZE, c->mem_task);
c->buf.tx = CAlloc(IMAP_BUFFER_SIZE, c->mem_task);
}
U0 @imap_free_buffers(ImapClient* c)
{
if (c->buf.rx)
Free(c->buf.rx);
if (c->buf.tx)
Free(c->buf.tx);
}
U0 @imap_send(ImapClient* c)
{
c->s->send(c->buf.tx, StrLen(c->buf.tx));
}
U0 @imap_set_response_text(ImapClient* c, U8* s)
{
// Trim whitespace at both ends
Bool trim = TRUE;
while (trim) {
switch (*s) {
case ' ':
case '\t':
case '\r':
case '\n':
++s;
break;
default:
trim = FALSE;
break;
}
}
StrCpy(c->response_text, s);
s = c->response_text + StrLen(c->response_text) - 1;
trim = TRUE;
while (trim) {
switch (*s) {
case ' ':
case '\t':
case '\r':
case '\n':
*s = NULL;
--s;
break;
default:
trim = FALSE;
break;
}
}
}
U0 @imap_error(ImapClient* c, U8* s)
{
c->state = IMAP_STATE_ERROR;
@imap_set_response_text(c, s);
}
U0 @imap_receive(ImapClient* c)
{
U8 buf[16];
I64 cnt = 0;
U8* msg;
while (c->s->state != TCP_SOCKET_STATE_CLOSED) {
cnt += c->s->receive(c->buf.rx + cnt, IMAP_BUFFER_SIZE);
if (StrFind("* OK", c->buf.rx)) {
@imap_set_response_text(c, StrFind("* OK", c->buf.rx) + 4);
return;
}
StrPrint(buf, "A%03d OK", c->tag);
if (StrFind(buf, c->buf.rx)) {
@imap_set_response_text(c, StrFind(buf, c->buf.rx) + 7);
return;
}
if (StrFind("* NO", c->buf.rx)) {
@imap_error(c, StrFind("* NO", c->buf.rx) + 4);
return;
}
StrPrint(buf, "A%03d NO", c->tag);
if (StrFind(buf, c->buf.rx)) {
@imap_error(c, StrFind(buf, c->buf.rx) + 7);
return;
}
if (StrFind("* BAD", c->buf.rx)) {
@imap_error(c, StrFind("* BAD", c->buf.rx) + 5);
return;
}
StrPrint(buf, "A%03d BAD", c->tag);
if (StrFind(buf, c->buf.rx)) {
@imap_error(c, StrFind(buf, c->buf.rx) + 8);
return;
}
}
}
U0 @imap_logout(ImapClient* c)
{
StrPrint(c->buf.tx, "A%03d LOGOUT\r\n", ++c->tag);
@imap_send(c);
// Receive data
@imap_receive(c);
c->state = IMAP_STATE_LOGOUT;
}
U0 @imap_close(ImapClient* c)
{
c->s->close();
@imap_free_buffers(c);
Free(c->s);
Free(c->close);
Free(c->connect);
Free(c->fetch);
Free(c->fetch_array);
Free(c->select);
Free(c->login);
Free(c->logout);
Free(c);
}
U0 @imap_login(ImapClient* c, U8* userid, U8* password)
{
StrPrint(c->buf.tx, "A%03d LOGIN %s %s\r\n", ++c->tag, userid, password);
@imap_send(c);
// Receive data
@imap_receive(c);
if (c->state == IMAP_STATE_ERROR)
return;
c->state = IMAP_STATE_AUTHENTICATED;
}
JsonArray* @imap_fetch_array(ImapClient* c, U8* sequence_set, U8* data_items)
{
StrPrint(c->buf.tx, "A%03d FETCH %s %s\r\n", ++c->tag, sequence_set, data_items);
@imap_send(c);
// Receive data
@imap_receive(c);
if (c->state == IMAP_STATE_ERROR)
return Json.Parse("[]", c->mem_task);
// Split buffer into lines
I64 i = 0;
I64 lines_cnt = 0;
U8** lines = @imap_lineate_buffer(c, &lines_cnt);
U8* param_list = NULL;
if (!lines_cnt) {
Free(lines);
return Json.Parse("[]", c->mem_task);
}
// Convert each param_list to a JSON array
JsonArray* a = Json.CreateArray(c->mem_task);
for (i = 0; i < lines_cnt; i++) {
param_list = StrFind(" FETCH (", lines[i]);
if (param_list) {
a->append(@imap_json_array_from_param_list(c, param_list + 7), JSON_ARRAY);
}
}
// Return array of arrays
return a;
}
U0 @imap_fetch(ImapClient* c, I64 uid)
{
StrPrint(c->buf.tx, "A%03d UID FETCH %d BODY.PEEK[]\r\n", ++c->tag, uid);
@imap_send(c);
@imap_receive(c);
}
U0 @imap_select(ImapClient* c, U8* mailbox)
{
StrPrint(c->buf.tx, "A%03d SELECT %s\r\n", ++c->tag, mailbox);
@imap_send(c);
@imap_receive(c);
if (c->state == IMAP_STATE_ERROR)
return;
StrCpy(&c->selected, mailbox);
c->state = IMAP_STATE_SELECTED;
}
U0 @imap_connect(ImapClient* c, U8* host, I64 port)
{
if (!host || !port || !StrLen(host))
return;
U32 addr = @dns_query(host);
if (addr == U32_MAX)
return;
TlsSocket* s = NULL;
switch (port) {
case 143:
s = @tcp_socket_create(host, port);
break;
case 993:
s = @tls_socket_create(host, port);
break;
default:
return;
}
if (!s)
return;
c->s = s;
// Receive data
@imap_receive(c);
if (c->state == IMAP_STATE_ERROR)
return;
c->state = IMAP_STATE_NOT_AUTHENTICATED;
}
U0 @imap_close_wrapper_function()
{
@imap_close(IMAP_WRAPPER_MAGIC_NUMBER);
}
U0 @imap_connect_wrapper_function(U8* host, I64 port)
{
@imap_connect(IMAP_WRAPPER_MAGIC_NUMBER, host, port);
}
U0 @imap_select_wrapper_function(U8* mailbox)
{
@imap_select(IMAP_WRAPPER_MAGIC_NUMBER, mailbox);
}
U0 @imap_login_wrapper_function(U8* userid, U8* password)
{
@imap_login(IMAP_WRAPPER_MAGIC_NUMBER, userid, password);
}
U0 @imap_logout_wrapper_function()
{
@imap_logout(IMAP_WRAPPER_MAGIC_NUMBER);
}
U0 @imap_fetch_wrapper_function(I64 uid)
{
@imap_fetch(IMAP_WRAPPER_MAGIC_NUMBER, uid);
}
U0 @imap_fetch_array_wrapper_function(U8* sequence_set, U8* data_items)
{
@imap_fetch_array(IMAP_WRAPPER_MAGIC_NUMBER, sequence_set, data_items);
}
ImapClient* @imap_new(CTask* mem_task)
{
if (!mem_task) {
return NULL;
}
ImapClient* c = CAlloc(sizeof(ImapClient), mem_task);
c->mem_task = mem_task;
c->o = Json.CreateObject(mem_task);
c->state = 0;
StrCpy(&c->selected, "");
@imap_init_buffers(c);
c->tag = 1;
U64 a;
I64 buffer_size = (MSize(&@imap_close_wrapper_function) + MSize(&@imap_connect_wrapper_function) + MSize(&@imap_fetch_wrapper_function) + MSize(&@imap_fetch_array_wrapper_function) + MSize(&@imap_select_wrapper_function) + MSize(&@imap_login_wrapper_function) + MSize(&@imap_logout_wrapper_function));
buffer_size += buffer_size % 16;
U64 code_ptr = CAlloc(buffer_size, c->mem_task->code_heap);
I64 code_size = 0;
// Create a copy of function and patch close
code_size = MSize(&@imap_close_wrapper_function);
c->close = code_ptr;
MemCpy(c->close, &@imap_close_wrapper_function, code_size);
code_ptr += code_size;
a = c->close;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_close);
// Create a copy of function and patch connect
code_size = MSize(&@imap_connect_wrapper_function);
c->connect = code_ptr;
MemCpy(c->connect, &@imap_connect_wrapper_function, code_size);
code_ptr += code_size;
a = c->connect;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_connect);
// Create a copy of function and patch select
code_size = MSize(&@imap_select_wrapper_function);
c->select = code_ptr;
MemCpy(c->select, &@imap_select_wrapper_function, code_size);
code_ptr += code_size;
a = c->select;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_select);
// Create a copy of function and patch login
code_size = MSize(&@imap_login_wrapper_function);
c->login = code_ptr;
MemCpy(c->login, &@imap_login_wrapper_function, code_size);
code_ptr += code_size;
a = c->login;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_login);
// Create a copy of function and patch logout
code_size = MSize(&@imap_logout_wrapper_function);
c->logout = code_ptr;
MemCpy(c->logout, &@imap_logout_wrapper_function, code_size);
code_ptr += code_size;
a = c->logout;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_logout);
// Create a copy of function and patch fetch
code_size = MSize(&@imap_fetch_wrapper_function);
c->fetch = code_ptr;
MemCpy(c->fetch, &@imap_fetch_wrapper_function, code_size);
code_ptr += code_size;
a = c->fetch;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_fetch);
// Create a copy of function and patch fetch_array
code_size = MSize(&@imap_fetch_array_wrapper_function);
c->fetch_array = code_ptr;
MemCpy(c->fetch_array, &@imap_fetch_array_wrapper_function, code_size);
code_ptr += code_size;
a = c->fetch_array;
while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
++a;
MemSetI64(a, c, 1);
a += 9;
@patch_call_rel32(a, &@imap_fetch_array);
return c;
}
"imap ";