/* $Id: imap4.c,v 1.20 2011/06/04 23:48:18 khorben Exp $ */ /* Copyright (c) 2011 Pierre Pronchery */ /* This file is part of DeforaOS Desktop Mailer */ /* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* FIXME: * - no longer block on connect() * - recovery on errors * - support multiple connections? */ #include #include #include #include #include #include #include #include #include #include "Mailer.h" /* IMAP4 */ /* private */ /* types */ typedef enum _IMAP4CommandStatus { I4CS_QUEUED = 0, I4CS_SENT, I4CS_ERROR, I4CS_PARSING, I4CS_OK } IMAP4CommandStatus; typedef enum _IMAP4Context { I4C_INIT = 0, I4C_FETCH, I4C_LIST, I4C_LOGIN, I4C_NOOP, I4C_SELECT } IMAP4Context; typedef struct _IMAP4Command { uint16_t id; IMAP4CommandStatus status; IMAP4Context context; char * buf; size_t buf_cnt; } IMAP4Command; typedef struct _IMAP4 { int fd; guint source; GIOChannel * channel; char * rd_buf; size_t rd_buf_cnt; guint rd_source; guint wr_source; IMAP4Command * queue; size_t queue_cnt; uint16_t queue_id; } IMAP4; /* variables */ static char const _imap4_type[] = "IMAP4"; static char const _imap4_name[] = "IMAP4 server"; AccountConfig _imap4_config[] = { { "username", "Username", ACT_STRING, NULL }, { "password", "Password", ACT_PASSWORD, NULL }, { "hostname", "Server hostname", ACT_STRING, NULL }, { "port", "Server port", ACT_UINT16, 994 }, #if 0 /* FIXME SSL is not supported yet */ { "ssl", "Use SSL", ACT_BOOLEAN, 1 }, #endif { "sent", "Sent mails folder", ACT_NONE, NULL }, { "draft", "Draft mails folder", ACT_NONE, NULL }, { NULL, NULL, ACT_NONE, NULL } }; /* prototypes */ /* plug-in */ static int _imap4_init(AccountPlugin * plugin); static int _imap4_destroy(AccountPlugin * plugin); /* useful */ static IMAP4Command * _imap4_command(AccountPlugin * plugin, IMAP4Context context, char const * command); static int _imap4_parse(AccountPlugin * plugin); /* callbacks */ static gboolean _on_idle(gpointer data); static gboolean _on_noop(gpointer data); static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition, gpointer data); /* public */ /* variables */ AccountPlugin account_plugin = { NULL, _imap4_type, _imap4_name, NULL, _imap4_config, _imap4_init, _imap4_destroy, NULL, NULL, NULL }; /* private */ /* imap4_init */ static int _imap4_init(AccountPlugin * plugin) { IMAP4 * imap4; if((imap4 = malloc(sizeof(*imap4))) == NULL) return -1; memset(imap4, 0, sizeof(*imap4)); plugin->priv = imap4; imap4->fd = -1; if((imap4->queue = malloc(sizeof(*imap4->queue))) == NULL) { free(imap4); return -1; } imap4->queue[0].id = 0; imap4->queue[0].context = I4C_INIT; imap4->queue[0].status = I4CS_SENT; imap4->queue[0].buf = NULL; imap4->queue[0].buf_cnt = 0; imap4->queue_cnt = 1; imap4->queue_id = 1; imap4->source = g_idle_add(_on_idle, plugin); return 0; } /* imap4_destroy */ static int _imap4_destroy(AccountPlugin * plugin) { IMAP4 * imap = plugin->priv; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif /* FIXME implement */ free(imap); return 0; } /* useful */ /* imap4_command */ static IMAP4Command * _imap4_command(AccountPlugin * plugin, IMAP4Context context, char const * command) { IMAP4 * imap4 = plugin->priv; IMAP4Command * p; size_t len; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, command); #endif if(command == NULL || (len = strlen(command) + 6) == 0) return NULL; len += 2; if((p = realloc(imap4->queue, sizeof(*p) * (imap4->queue_cnt + 1))) == NULL) return NULL; imap4->queue = p; p = &imap4->queue[imap4->queue_cnt]; p->id = imap4->queue_id++; p->context = context; p->status = I4CS_QUEUED; if((p->buf = malloc(len + 1)) == NULL) return NULL; p->buf_cnt = snprintf(p->buf, len + 1, "a%04x %s\r\n", p->id, command); #if 0 /* XXX later */ memset(&p->data, 0, sizeof(p->data)); #endif if(imap4->queue_cnt++ == 0) { if(imap4->source != 0) { /* cancel the pending NOOP operation */ g_source_remove(imap4->source); imap4->source = 0; } imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_write, plugin); } return p; } /* imap4_parse */ static int _parse_context(AccountPlugin * plugin, char const * answer); static int _imap4_parse(AccountPlugin * plugin) { AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; size_t i; size_t j; IMAP4Command * cmd; char buf[8]; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif for(i = 0, j = 0;; j = ++i) { for(; i < imap4->rd_buf_cnt; i++) if(imap4->rd_buf[i] == '\r' && i + 1 < imap4->rd_buf_cnt && imap4->rd_buf[++i] == '\n') break; if(i == imap4->rd_buf_cnt) break; if(imap4->queue_cnt == 0) continue; imap4->rd_buf[i - 1] = '\0'; cmd = &imap4->queue[0]; if(cmd->status == I4CS_SENT) { snprintf(buf, sizeof(buf), "a%04x ", cmd->id); #ifdef DEBUG fprintf(stderr, "DEBUG: %s() expecting \"%s\"\n", __func__, buf); #endif if(strncmp(&imap4->rd_buf[j], "* ", 2) == 0) j += 2; else if(strncmp(&imap4->rd_buf[j], buf, 6) == 0) { j += 6; cmd->status = I4CS_PARSING; if(strncmp("BAD ", &imap4->rd_buf[j], 4) == 0) helper->error(helper->account, &imap4->rd_buf[j + 4], 1); } } if(_parse_context(plugin, &imap4->rd_buf[j]) != 0) cmd->status = I4CS_ERROR; } if(j != 0) { imap4->rd_buf_cnt -= j; memmove(imap4->rd_buf, &imap4->rd_buf[j], imap4->rd_buf_cnt); } if(imap4->queue == NULL) return 0; return (imap4->queue[0].status != I4CS_ERROR) ? 0 : -1; } static int _parse_context(AccountPlugin * plugin, char const * answer) { int ret = -1; AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; char const * p; char const * q; gchar * r; char const list[] = "LIST \"*\" \"*\""; char buf[32]; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\") %u, %u\n", __func__, answer, cmd->context, cmd->status); #endif switch(cmd->context) { case I4C_FETCH: /* FIXME implement */ if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; return 0; case I4C_INIT: cmd->status = I4CS_OK; if((p = plugin->config[0].value) == NULL || *p == '\0') return -1; if((q = plugin->config[1].value) == NULL || *q == '\0') return -1; r = g_strdup_printf("%s %s %s", "LOGIN", p, q); cmd = _imap4_command(plugin, I4C_LOGIN, r); g_free(r); return (cmd != NULL) ? 0 : -1; case I4C_LIST: p = answer; if(strncmp("OK", p, 2) == 0) { cmd->status = I4CS_OK; return 0; } if(strncmp("LIST ", p, 5) != 0) return -1; p += 5; if(*p == '(') for(p++; *p != '\0' && *p++ != ')';); if(*p == ' ') /* skip spaces */ for(p++; *p != '\0' && *p == ' '; p++); if(*p == '\"') /* skip reference */ for(p++; *p != '\0' && *p++ != '\"';); if(*p == ' ') /* skip spaces */ for(p++; *p != '\0' && *p == ' '; p++); if(*p == '\"' && sscanf(++p, "%31[^\"]", buf) == 1) { /* FIXME create metadata for this folder */ helper->folder_new(helper->account, NULL, NULL, FT_INBOX, buf); buf[31] = '\0'; r = g_strdup_printf("%s \"%s\"", "SELECT", buf); cmd = _imap4_command(plugin, I4C_SELECT, r); g_free(r); } return (cmd != NULL) ? 0 : -1; case I4C_LOGIN: if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; return (_imap4_command(plugin, I4C_LIST, list) != NULL) ? 0 : -1; case I4C_NOOP: if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; return 0; case I4C_SELECT: if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; p = "FETCH 1:* (FLAGS ENVELOPE)"; cmd = _imap4_command(plugin, I4C_FETCH, p); return (cmd != NULL) ? 0 : -1; } return ret; } /* callbacks */ /* on_idle */ static gboolean _idle_connect(AccountPlugin * plugin); static gboolean _idle_channel(AccountPlugin * plugin); static gboolean _on_idle(gpointer data) { gboolean ret = FALSE; AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif if(imap4->fd < 0) ret = _idle_connect(plugin); else if(imap4->channel == NULL) ret = _idle_channel(plugin); if(ret == FALSE) imap4->source = 0; return ret; } static gboolean _idle_connect(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; char const * p; struct hostent * he; unsigned short port; struct sockaddr_in sa; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif /* FIXME report errors */ if((p = plugin->config[2].value) == NULL) return FALSE; if((he = gethostbyname(p)) == NULL) { plugin->helper->error(NULL, hstrerror(h_errno), 1); return FALSE; } if((p = plugin->config[3].value) == NULL) return FALSE; port = (unsigned long)p; if((imap4->fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { plugin->helper->error(NULL, strerror(errno), 1); return FALSE; } sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = *((uint32_t*)he->h_addr_list[0]); #ifdef DEBUG fprintf(stderr, "DEBUG: %s() connecting to %s:%u\n", __func__, inet_ntoa(sa.sin_addr), port); #endif if(connect(imap4->fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) { plugin->helper->error(NULL, strerror(errno), 1); close(imap4->fd); imap4->fd = -1; return FALSE; } return TRUE; } static gboolean _idle_channel(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; GError * error = NULL; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif imap4->channel = g_io_channel_unix_new(imap4->fd); g_io_channel_set_encoding(imap4->channel, NULL, &error); g_io_channel_set_buffered(imap4->channel, FALSE); imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_read, plugin); return TRUE; } /* on_noop */ static gboolean _on_noop(gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; _imap4_command(plugin, I4C_NOOP, "NOOP"); imap4->source = 0; return FALSE; } /* on_watch_can_read */ static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; char * p; gsize cnt = 0; GError * error = NULL; GIOStatus status; IMAP4Command * cmd; if(condition != G_IO_IN || source != imap4->channel) return FALSE; /* should not happen */ if((p = realloc(imap4->rd_buf, imap4->rd_buf_cnt + 256)) == NULL) return TRUE; /* XXX retries immediately (delay?) */ imap4->rd_buf = p; status = g_io_channel_read_chars(source, &imap4->rd_buf[imap4->rd_buf_cnt], 256, &cnt, &error); #ifdef DEBUG fprintf(stderr, "%s", "DEBUG: IMAP4 SERVER: "); fwrite(&imap4->rd_buf[imap4->rd_buf_cnt], sizeof(*p), cnt, stderr); #endif imap4->rd_buf_cnt += cnt; switch(status) { case G_IO_STATUS_NORMAL: break; case G_IO_STATUS_ERROR: plugin->helper->error(NULL, error->message, 1); case G_IO_STATUS_EOF: default: /* XXX find a way to recover */ imap4->rd_source = 0; return FALSE; } if(_imap4_parse(plugin) != 0 || imap4->queue_cnt == 0) { imap4->rd_source = 0; return FALSE; } cmd = &imap4->queue[0]; if(cmd->buf_cnt == 0) { if(cmd->status == I4CS_SENT || cmd->status == I4CS_PARSING) return TRUE; else if(cmd->status == I4CS_OK) memmove(cmd, &imap4->queue[1], sizeof(*cmd) * --imap4->queue_cnt); } imap4->rd_source = 0; if(imap4->queue_cnt == 0) imap4->source = g_timeout_add(30000, _on_noop, plugin); else imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_write, plugin); return FALSE; } /* on_watch_can_write */ static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; gsize cnt = 0; GError * error = NULL; GIOStatus status; char * p; if(condition != G_IO_OUT || source != imap4->channel || imap4->queue_cnt == 0 || cmd->buf_cnt == 0) return FALSE; /* should not happen */ status = g_io_channel_write_chars(source, cmd->buf, cmd->buf_cnt, &cnt, &error); #ifdef DEBUG fprintf(stderr, "%s", "DEBUG: IMAP4 CLIENT: "); fwrite(cmd->buf, sizeof(*p), cnt, stderr); #endif if(cnt != 0) { cmd->buf_cnt -= cnt; memmove(cmd->buf, &cmd->buf[cnt], cmd->buf_cnt); if((p = realloc(cmd->buf, cmd->buf_cnt)) != NULL) cmd->buf = p; /* we can ignore errors... */ else if(cmd->buf_cnt == 0) cmd->buf = NULL; /* ...except when it's not one */ } switch(status) { case G_IO_STATUS_NORMAL: break; case G_IO_STATUS_ERROR: plugin->helper->error(NULL, error->message, 1); case G_IO_STATUS_EOF: default: /* XXX find a way to recover */ imap4->wr_source = 0; return FALSE; } if(cmd->buf_cnt > 0) return TRUE; cmd->status = I4CS_SENT; imap4->wr_source = 0; if(imap4->rd_source == 0) imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_read, plugin); return FALSE; }