From df4a9a484aa6ddae251088bb0c6eef706d4dada2 Mon Sep 17 00:00:00 2001 From: Alec Murphy Date: Wed, 24 Sep 2025 15:56:44 -0400 Subject: [PATCH] System/Libraries/Imap: Add initial support for IMAP protocol --- System/Libraries/Imap.HC | 517 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 System/Libraries/Imap.HC diff --git a/System/Libraries/Imap.HC b/System/Libraries/Imap.HC new file mode 100644 index 0000000..9c340ec --- /dev/null +++ b/System/Libraries/Imap.HC @@ -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 ";