/* $Id: imap4.c,v 1.56 2011/09/21 13:24:57 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: * - openssl should be more explicit when SSL_set_fd() is missing (no BIO) * - support multiple connections? */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Mailer.h" /* IMAP4 */ /* private */ /* types */ struct _AccountFolder { Folder * folder; char * name; AccountMessage ** messages; size_t messages_cnt; AccountFolder ** folders; size_t folders_cnt; }; struct _AccountMessage { Message * message; unsigned int id; }; typedef enum _IMAP4CommandStatus { I4CS_QUEUED = 0, I4CS_SENT, I4CS_ERROR, I4CS_PARSING, I4CS_OK } IMAP4CommandStatus; typedef enum _IMAP4ConfigValue { I4CV_USERNAME = 0, I4CV_PASSWORD, I4CV_HOSTNAME, I4CV_PORT, I4CV_SSL, I4CV_PADDING0, I4CV_PREFIX } IMAP4Config; #define I4CV_LAST I4CV_PREFIX #define I4CV_COUNT (I4CV_LAST + 1) typedef enum _IMAP4Context { I4C_INIT = 0, I4C_FETCH, I4C_LIST, I4C_LOGIN, I4C_NOOP, I4C_SELECT, I4C_STATUS } IMAP4Context; typedef enum _IMAP4FetchStatus { I4FS_ID = 0, I4FS_HEADERS, I4FS_BODY } IMAP4FetchStatus; typedef struct _IMAP4Command { uint16_t id; IMAP4CommandStatus status; IMAP4Context context; char * buf; size_t buf_cnt; union { struct { AccountFolder * folder; AccountMessage * message; unsigned int id; IMAP4FetchStatus status; } fetch; struct { AccountFolder * parent; } list; struct { AccountFolder * folder; AccountMessage * message; } select; struct { AccountFolder * folder; } status; } data; } IMAP4Command; typedef struct _IMAP4 { int fd; SSL_CTX * ssl_ctx; SSL * ssl; 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; AccountFolder folders; } IMAP4; /* variables */ static char const _imap4_type[] = "IMAP4"; static char const _imap4_name[] = "IMAP4 server"; AccountConfig _imap4_config[I4CV_COUNT + 1] = { { "username", "Username", ACT_STRING, NULL }, { "password", "Password", ACT_PASSWORD, NULL }, { "hostname", "Server hostname", ACT_STRING, NULL }, { "port", "Server port", ACT_UINT16, 143 }, { "ssl", "Use SSL", ACT_BOOLEAN, 0 }, #if 0 /* XXX not implemented yet */ { "sent", "Sent mails folder", ACT_NONE, NULL }, { "draft", "Draft mails folder", ACT_NONE, NULL }, #endif { NULL, NULL, ACT_SEPARATOR, NULL }, { "prefix", "Prefix", ACT_STRING, NULL }, { NULL, NULL, ACT_NONE, NULL } }; /* prototypes */ /* plug-in */ static int _imap4_init(AccountPlugin * plugin); static int _imap4_destroy(AccountPlugin * plugin); static int _imap4_refresh(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message); /* useful */ static IMAP4Command * _imap4_command(AccountPlugin * plugin, IMAP4Context context, char const * command); static int _imap4_parse(AccountPlugin * plugin); static void _imap4_reset(AccountPlugin * plugin); static AccountFolder * _imap4_folder_new(AccountPlugin * plugin, AccountFolder * parent, char const * name); static void _imap4_folder_delete(AccountPlugin * plugin, AccountFolder * folder); static AccountFolder * _imap4_folder_get_folder(AccountPlugin * plugin, AccountFolder * folder, char const * name); static AccountMessage * _imap4_folder_get_message(AccountPlugin * plugin, AccountFolder * folder, unsigned int id); static AccountMessage * _imap4_message_new(AccountPlugin * plugin, AccountFolder * folder, unsigned int id); static void _imap4_message_delete(AccountPlugin * plugin, AccountMessage * message); /* callbacks */ static gboolean _on_connect(gpointer data); static gboolean _on_noop(gpointer data); static gboolean _on_reset(gpointer data); static gboolean _on_watch_can_connect(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_handshake(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_read_ssl(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition, gpointer data); static gboolean _on_watch_can_write_ssl(GIOChannel * source, GIOCondition condition, gpointer data); /* public */ /* variables */ AccountPlugin account_plugin = { NULL, _imap4_type, _imap4_name, NULL, _imap4_config, _imap4_init, _imap4_destroy, NULL, _imap4_refresh, 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; SSL_load_error_strings(); SSL_library_init(); imap4->source = g_idle_add(_on_connect, plugin); return 0; } /* imap4_destroy */ static int _imap4_destroy(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif if(imap4 == NULL) /* XXX _imap4_destroy() may be called uninitialized */ return 0; _imap4_reset(plugin); #if 0 /* XXX do not free() */ _imap4_folder_delete(plugin, &imap4->folders); #endif free(imap4); return 0; } /* imap4_refresh */ static int _imap4_refresh(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message) { IMAP4Command * cmd; int len; char * buf; #ifdef DEBUG fprintf(stderr, "DEBUG: %s() %u\n", __func__, (message != NULL) ? message->id : 0); #endif if((len = snprintf(NULL, 0, "EXAMINE \"%s\"", folder->name)) < 0 || (buf = malloc(++len)) == NULL) return -1; snprintf(buf, len, "EXAMINE \"%s\"", folder->name); cmd = _imap4_command(plugin, I4C_SELECT, buf); free(buf); if(cmd == NULL) return -1; cmd->data.select.folder = folder; cmd->data.select.message = message; 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 /* abort if the command is invalid */ if(command == NULL || (len = strlen(command) + 6) == 0) return NULL; /* abort if there is no active connection */ if(imap4->channel == NULL) return NULL; /* queue the command */ 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); memset(&p->data, 0, sizeof(p->data)); 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, (imap4->ssl != NULL) ? _on_watch_can_write_ssl : _on_watch_can_write, plugin); } return p; } /* imap4_parse */ static int _parse_context(AccountPlugin * plugin, char const * answer); static int _context_fetch(AccountPlugin * plugin, char const * answer); static int _context_init(AccountPlugin * plugin); static int _context_list(AccountPlugin * plugin, char const * answer); static int _context_login(AccountPlugin * plugin, char const * answer); static int _context_select(AccountPlugin * plugin); static int _context_status(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; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\") %u, %u\n", __func__, answer, cmd->context, cmd->status); #endif switch(cmd->context) { case I4C_FETCH: return _context_fetch(plugin, answer); case I4C_INIT: return _context_init(plugin); case I4C_LIST: return _context_list(plugin, answer); case I4C_LOGIN: return _context_login(plugin, answer); case I4C_NOOP: if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; return 0; case I4C_SELECT: return _context_select(plugin); case I4C_STATUS: return _context_status(plugin, answer); } return ret; } static int _context_fetch(AccountPlugin * plugin, char const * answer) { AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; AccountFolder * folder = cmd->data.fetch.folder; AccountMessage * message = cmd->data.fetch.message; unsigned int id = cmd->data.fetch.id; char * p; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(%p, \"%s\")\n", __func__, (void *)folder, answer); #endif if(cmd->status == I4CS_PARSING) { cmd->status = I4CS_OK; return 0; } switch(cmd->data.fetch.status) { case I4FS_BODY: #if 1 /* FIXME dirty hack for now */ if(strcmp(answer, ")") == 0) { cmd->data.fetch.status = I4FS_ID; return 0; } #endif helper->message_set_body(message->message, answer, strlen(answer), 1); helper->message_set_body(message->message, "\r\n", 2, 1); return 0; case I4FS_ID: id = strtol(answer, &p, 10); if(answer[0] == '\0' || *p != ' ') return 0; if(cmd->data.fetch.id != id) cmd->data.fetch.id = id; #ifdef DEBUG fprintf(stderr, "DEBUG: %s() %u\n", __func__, id); #endif answer = p; if(strncmp(answer, " FETCH ", 7) != 0) return 0; if((message = _imap4_folder_get_message(plugin, folder, id)) != NULL) { cmd->data.fetch.status = I4FS_HEADERS; cmd->data.fetch.message = message; } return (message != NULL) ? 0 : -1; case I4FS_HEADERS: if(strcmp(answer, "") == 0) { cmd->data.fetch.status = I4FS_BODY; helper->message_set_body(message->message, NULL, 0, 0); } else helper->message_set_header(message->message, answer); return 0; } return -1; } static int _context_init(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; char const * p; char const * q; gchar * r; cmd->status = I4CS_OK; if((p = plugin->config[I4CV_USERNAME].value) == NULL || *p == '\0') return -1; if((q = plugin->config[I4CV_PASSWORD].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; } static int _context_list(AccountPlugin * plugin, char const * answer) { IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; AccountFolder * folder; AccountFolder * parent = cmd->data.list.parent; char const * p; gchar * q; char const haschildren[] = "\\HasChildren"; int recurse = 0; char reference = '\0'; char buf[64]; 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 == '(') /* parse flags */ { for(p++; *p != '\0' && *p != ')'; p++) if(*p != '\\') continue; else if(strncmp(p, haschildren, sizeof(haschildren) - 1) == 0) /* FIXME */ { p += sizeof(haschildren) - 2; recurse = 1; } /* FIXME else skip until end of flag */ p++; } if(*p == ' ') /* skip spaces */ for(p++; *p != '\0' && *p == ' '; p++); if(*p == '\"') /* skip reference */ { if(p[1] != '\0' && p[1] != '"' && p[2] == '"') reference = p[1]; for(p++; *p != '\0' && *p++ != '\"';); } if(*p == ' ') /* skip spaces */ for(p++; *p != '\0' && *p == ' '; p++); if(*p == '\"' && sscanf(++p, "%63[^\"]", buf) == 1 && (folder = _imap4_folder_get_folder(plugin, parent, buf)) != NULL) { buf[63] = '\0'; /* FIXME escape the mailbox name (double quotes...) */ q = g_strdup_printf("%s \"%s\" (%s)", "STATUS", buf, "MESSAGES RECENT UNSEEN"); if((cmd = _imap4_command(plugin, I4C_STATUS, q)) != NULL) cmd->data.status.folder = folder; g_free(q); if(cmd != NULL && recurse == 1 && reference != '\0') { q = g_strdup_printf("%s \"\" \"%s%c%%\"", "LIST", buf, reference); if((cmd = _imap4_command(plugin, I4C_LIST, q)) != NULL) cmd->data.list.parent = folder; g_free(q); } } return (cmd != NULL) ? 0 : -1; } static int _context_login(AccountPlugin * plugin, char const * answer) { AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; char const * prefix = plugin->config[I4CV_PREFIX].value; gchar * q; if(cmd->status != I4CS_PARSING) return 0; if(strncmp("OK", answer, 2) != 0) return -helper->error(helper->account, "Authentication failed", 1); cmd->status = I4CS_OK; if((q = g_strdup_printf("%s \"\" \"%s%%\"", "LIST", (prefix != NULL) ? prefix : "")) == NULL) return -1; cmd = _imap4_command(plugin, I4C_LIST, q); g_free(q); if(cmd == NULL) return -1; cmd->data.list.parent = &imap4->folders; return 0; } static int _context_select(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; AccountFolder * folder; AccountMessage * message; char buf[32]; if(cmd->status != I4CS_PARSING) return 0; cmd->status = I4CS_OK; if((folder = cmd->data.select.folder) == NULL) return 0; /* XXX really is an error */ if((message = cmd->data.select.message) == NULL) /* FIXME queue commands in batches instead */ snprintf(buf, sizeof(buf), "%s %s %s", "FETCH", "1:*", "BODY.PEEK[HEADER]"); else snprintf(buf, sizeof(buf), "%s %u %s", "FETCH", message->id, "BODY.PEEK[]"); if((cmd = _imap4_command(plugin, I4C_FETCH, buf)) == NULL) return -1; cmd->data.fetch.folder = folder; cmd->data.fetch.message = message; cmd->data.fetch.id = (message != NULL) ? message->id : 0; cmd->data.fetch.status = I4FS_ID; return 0; } static int _context_status(AccountPlugin * plugin, char const * answer) { IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; char const * p; char const messages[] = "MESSAGES"; char const recent[] = "RECENT"; char const unseen[] = "UNSEEN"; unsigned int u; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, answer); #endif p = answer; if(strncmp("OK", p, 2) == 0) { cmd->status = I4CS_OK; return 0; } if(strncmp("STATUS ", p, 7) != 0) return 0; p += 7; if(*p == '\"') /* skip reference */ for(p++; *p != '\0' && *p++ != '\"';); if(*p == ' ') /* skip spaces */ for(p++; *p != '\0' && *p == ' '; p++); if(*p == '(') for(p++; *p != '\0' && *p != ')'; p++) { if(strncmp(p, messages, sizeof(messages) - 1) == 0 && p[sizeof(messages) - 1] == ' ') { p += sizeof(messages); sscanf(p, "%u", &u); /* FIXME really implement */ } if(strncmp(p, recent, sizeof(recent) - 1) == 0 && p[sizeof(recent) - 1] == ' ') { p += sizeof(recent); sscanf(p, "%u", &u); /* FIXME really implement */ } if(strncmp(p, unseen, sizeof(unseen) - 1) == 0 && p[sizeof(unseen) - 1] == ' ') { p += sizeof(unseen); sscanf(p, "%u", &u); /* FIXME really implement */ } /* FIXME implement more */ /* skip until the next space */ for(; *p != '\0' && *p != ' '; p++); } return 0; } /* imap4_reset */ static void _imap4_reset(AccountPlugin * plugin) { IMAP4 * imap4 = plugin->priv; size_t i; if(imap4->rd_source != 0) g_source_remove(imap4->rd_source); imap4->rd_source = 0; if(imap4->wr_source != 0) g_source_remove(imap4->wr_source); imap4->wr_source = 0; if(imap4->source != 0) g_source_remove(imap4->source); imap4->source = 0; if(imap4->channel != NULL) { g_io_channel_shutdown(imap4->channel, TRUE, NULL); g_io_channel_unref(imap4->channel); imap4->fd = -1; } imap4->channel = NULL; for(i = 0; i < imap4->queue_cnt; i++) free(imap4->queue[i].buf); free(imap4->queue); imap4->queue = NULL; imap4->queue_cnt = 0; if(imap4->ssl_ctx != NULL) SSL_CTX_free(imap4->ssl_ctx); imap4->ssl_ctx = NULL; if(imap4->fd >= 0) close(imap4->fd); imap4->fd = -1; } /* imap4_folder_new */ static AccountFolder * _imap4_folder_new(AccountPlugin * plugin, AccountFolder * parent, char const * name) { AccountPluginHelper * helper = plugin->helper; AccountFolder * folder; AccountFolder ** p; IMAP4 * imap4 = plugin->priv; FolderType type = FT_FOLDER; struct { char const * name; FolderType type; } name_type[] = { { "Inbox", FT_INBOX }, { "Drafts", FT_DRAFTS }, { "Trash", FT_TRASH }, { "Sent", FT_SENT } }; size_t i; size_t len; if((p = realloc(parent->folders, sizeof(*p) * (parent->folders_cnt + 1))) == NULL) return NULL; parent->folders = p; if((folder = object_new(sizeof(*folder))) == NULL) return NULL; folder->name = strdup(name); if(parent == &imap4->folders) for(i = 0; i < sizeof(name_type) / sizeof(*name_type); i++) if(strcasecmp(name_type[i].name, name) == 0) { type = name_type[i].type; name = name_type[i].name; break; } /* shorten the name as required if has a parent */ if(parent->name != NULL && (len = strlen(parent->name)) > 0 && strncmp(name, parent->name, len) == 0) name = &name[len + 1]; folder->folder = helper->folder_new(helper->account, folder, parent->folder, type, name); folder->messages = NULL; folder->messages_cnt = 0; folder->folders = NULL; folder->folders_cnt = 0; if(folder->folder == NULL || folder->name == NULL) { _imap4_folder_delete(plugin, folder); return NULL; } parent->folders[parent->folders_cnt++] = folder; return folder; } /* imap4_folder_delete */ static void _imap4_folder_delete(AccountPlugin * plugin, AccountFolder * folder) { size_t i; if(folder->folder != NULL) plugin->helper->folder_delete(folder->folder); free(folder->name); for(i = 0; i < folder->messages_cnt; i++) _imap4_message_delete(plugin, folder->messages[i]); free(folder->messages); for(i = 0; i < folder->folders_cnt; i++) _imap4_folder_delete(plugin, folder->folders[i]); free(folder->folders); object_delete(folder); } /* imap4_folder_get_folder */ static AccountFolder * _imap4_folder_get_folder(AccountPlugin * plugin, AccountFolder * folder, char const * name) { size_t i; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\", \"%s\")\n", __func__, folder->name, name); #endif for(i = 0; i < folder->folders_cnt; i++) if(strcmp(folder->folders[i]->name, name) == 0) return folder->folders[i]; return _imap4_folder_new(plugin, folder, name); } /* imap4_folder_get_message */ static AccountMessage * _imap4_folder_get_message(AccountPlugin * plugin, AccountFolder * folder, unsigned int id) { size_t i; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\", %u)\n", __func__, folder->name, id); #endif for(i = 0; i < folder->messages_cnt; i++) if(folder->messages[i]->id == id) return folder->messages[i]; return _imap4_message_new(plugin, folder, id); } /* imap4_message_new */ static AccountMessage * _imap4_message_new(AccountPlugin * plugin, AccountFolder * folder, unsigned int id) { AccountPluginHelper * helper = plugin->helper; AccountMessage * message; AccountMessage ** p; if((p = realloc(folder->messages, sizeof(*p) * (folder->messages_cnt + 1))) == NULL) return NULL; folder->messages = p; if((message = object_new(sizeof(*message))) == NULL) return NULL; message->id = id; if((message->message = helper->message_new(helper->account, folder->folder, message)) == NULL) { _imap4_message_delete(plugin, message); return NULL; } folder->messages[folder->messages_cnt++] = message; return message; } /* imap4_message_delete */ static void _imap4_message_delete(AccountPlugin * plugin, AccountMessage * message) { if(message->message != NULL) plugin->helper->message_delete(message->message); object_delete(message); } /* callbacks */ /* on_idle */ static int _connect_channel(AccountPlugin * plugin); static gboolean _on_connect(gpointer data) { AccountPlugin * plugin = data; AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; char const * hostname; char const * p; struct hostent * he; unsigned short port; struct sockaddr_in sa; int res; char buf[128]; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif imap4->source = 0; if((hostname = plugin->config[I4CV_HOSTNAME].value) == NULL) { helper->error(NULL, "No hostname set", 1); return FALSE; } if((he = gethostbyname(hostname)) == NULL) { helper->error(NULL, hstrerror(h_errno), 1); return _on_reset(plugin); } if((p = plugin->config[I4CV_PORT].value) == NULL) return FALSE; port = (unsigned long)p; /* setup SSL */ if(plugin->config[I4CV_SSL].value != NULL) { /* FIXME obtain the SSL context from Mailer itself */ if((imap4->ssl_ctx = SSL_CTX_new(SSLv3_client_method())) == NULL || SSL_CTX_set_cipher_list(imap4->ssl_ctx, SSL_DEFAULT_CIPHER_LIST) != 1 || SSL_CTX_load_verify_locations(imap4->ssl_ctx, NULL, "/etc/openssl") != 1) { helper->error(NULL, ERR_error_string(ERR_get_error(), buf), 1); return _on_reset(plugin); } #if 0 /* XXX nicer for the server (knows why we shutdown) but not for us */ SSL_CTX_set_verify(imap4->ssl_ctx, SSL_VERIFY_PEER, NULL); #endif } if((imap4->fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { helper->error(NULL, strerror(errno), 1); return _on_reset(plugin); } if((res = fcntl(imap4->fd, F_GETFL)) >= 0 && fcntl(imap4->fd, F_SETFL, res | O_NONBLOCK) == -1) /* ignore this error */ helper->error(NULL, strerror(errno), 1); sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = *((uint32_t*)he->h_addr_list[0]); helper->status(helper->account, "Connecting to %s (%s:%u)", hostname, inet_ntoa(sa.sin_addr), port); if((connect(imap4->fd, (struct sockaddr *)&sa, sizeof(sa)) != 0 && errno != EINPROGRESS) || _connect_channel(plugin) != 0) { helper->error(NULL, strerror(errno), 1); return _on_reset(plugin); } imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_connect, plugin); return FALSE; } static int _connect_channel(AccountPlugin * plugin) { AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; GError * error = NULL; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif /* prepare queue */ if((imap4->queue = malloc(sizeof(*imap4->queue))) == NULL) return -helper->error(helper->account, strerror(errno), 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; /* setup channel */ 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); return 0; } /* 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_reset */ static gboolean _on_reset(gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; _imap4_reset(plugin); imap4->source = g_timeout_add(3000, _on_connect, plugin); return FALSE; } /* on_watch_can_connect */ static gboolean _on_watch_can_connect(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; char buf[128]; if(condition != G_IO_OUT || source != imap4->channel) return FALSE; /* should not happen */ #ifdef DEBUG fprintf(stderr, "DEBUG: %s() connected\n", __func__); #endif imap4->wr_source = 0; /* setup SSL */ if(imap4->ssl_ctx != NULL) { if((imap4->ssl = SSL_new(imap4->ssl_ctx)) == NULL) { helper->error(NULL, ERR_error_string(ERR_get_error(), buf), 1); return FALSE; } if(SSL_set_fd(imap4->ssl, imap4->fd) != 1) ; /* FIXME handle error */ SSL_set_connect_state(imap4->ssl); /* perform initial handshake */ imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_handshake, plugin); return FALSE; } /* wait for the server's banner */ imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_read, plugin); return FALSE; } /* on_watch_can_handshake */ static int _handshake_verify(AccountPlugin * plugin); static gboolean _on_watch_can_handshake(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; int res; int err; char buf[128]; if((condition != G_IO_IN && condition != G_IO_OUT) || source != imap4->channel || imap4->ssl == NULL) return FALSE; /* should not happen */ #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif imap4->wr_source = 0; imap4->rd_source = 0; if((res = SSL_do_handshake(imap4->ssl)) == 1) { if(_handshake_verify(plugin) != 0) return _on_reset(plugin); /* wait for the server's banner */ imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_read_ssl, plugin); return FALSE; } err = SSL_get_error(imap4->ssl, res); ERR_error_string(err, buf); if(res == 0) { helper->error(helper->account, buf, 1); return _on_reset(plugin); } if(err == SSL_ERROR_WANT_WRITE) imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_handshake, plugin); else if(err == SSL_ERROR_WANT_READ) imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_handshake, plugin); else { helper->error(helper->account, buf, 1); return _on_reset(plugin); } return FALSE; } static int _handshake_verify(AccountPlugin * plugin) { AccountPluginHelper * helper = plugin->helper; IMAP4 * imap4 = plugin->priv; X509 * x509; char buf[256] = ""; if(SSL_get_verify_result(imap4->ssl) != X509_V_OK) return helper->confirm(helper->account, "The certificate could" " not be verified.\nConnect anyway?"); x509 = SSL_get_peer_certificate(imap4->ssl); X509_NAME_get_text_by_NID(X509_get_subject_name(x509), NID_commonName, buf, sizeof(buf)); if(strcasecmp(buf, plugin->config[I4CV_HOSTNAME].value) != 0) return helper->confirm(helper->account, "The certificate could" " not be matched.\nConnect anyway?"); return 0; } /* 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: imap4->rd_source = g_idle_add(_on_reset, plugin); return FALSE; } if(_imap4_parse(plugin) != 0) { imap4->rd_source = g_idle_add(_on_reset, plugin); return FALSE; } if(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 || cmd->status == I4CS_ERROR) 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_read_ssl */ static gboolean _on_watch_can_read_ssl(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; char * p; int cnt; IMAP4Command * cmd; char buf[128]; const int inc = 1024; /* FIXME does not work with smaller values */ #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif if(source != imap4->channel) return FALSE; /* should not happen */ if((p = realloc(imap4->rd_buf, imap4->rd_buf_cnt + inc)) == NULL) return TRUE; /* XXX retries immediately (delay?) */ imap4->rd_buf = p; if((cnt = SSL_read(imap4->ssl, &imap4->rd_buf[imap4->rd_buf_cnt], inc)) <= 0) { if(cnt < 0 && SSL_get_error(imap4->ssl, cnt) == SSL_ERROR_WANT_WRITE) { imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_read_ssl, plugin); return FALSE; } else if(cnt < 0 && SSL_get_error(imap4->ssl, cnt) == SSL_ERROR_WANT_READ) { imap4->rd_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_read_ssl, plugin); return FALSE; } ERR_error_string(SSL_get_error(imap4->ssl, cnt), buf); plugin->helper->error(NULL, buf, 1); imap4->rd_source = g_idle_add(_on_reset, plugin); return FALSE; } #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; if(_imap4_parse(plugin) != 0) { imap4->rd_source = g_idle_add(_on_reset, plugin); return FALSE; } if(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 || cmd->status == I4CS_ERROR) 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_ssl, 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; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif 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: imap4->wr_source = g_idle_add(_on_reset, plugin); 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; } /* on_watch_can_write_ssl */ static gboolean _on_watch_can_write_ssl(GIOChannel * source, GIOCondition condition, gpointer data) { AccountPlugin * plugin = data; IMAP4 * imap4 = plugin->priv; IMAP4Command * cmd = &imap4->queue[0]; int cnt; char * p; char buf[128]; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif if(source != imap4->channel || imap4->queue_cnt == 0 || cmd->buf_cnt == 0) return FALSE; /* should not happen */ if((cnt = SSL_write(imap4->ssl, cmd->buf, cmd->buf_cnt)) <= 0) { if(cnt < 0 && SSL_get_error(imap4->ssl, cnt) == SSL_ERROR_WANT_READ) { imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_IN, _on_watch_can_write_ssl, plugin); return FALSE; } else if(cnt < 0 && SSL_get_error(imap4->ssl, cnt) == SSL_ERROR_WANT_WRITE) { imap4->wr_source = g_io_add_watch(imap4->channel, G_IO_OUT, _on_watch_can_write_ssl, plugin); return FALSE; } ERR_error_string(SSL_get_error(imap4->ssl, cnt), buf); plugin->helper->error(NULL, buf, 1); imap4->wr_source = g_idle_add(_on_reset, plugin); return FALSE; } #ifdef DEBUG fprintf(stderr, "%s", "DEBUG: IMAP4 CLIENT: "); fwrite(cmd->buf, sizeof(*p), cnt, stderr); #endif 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 */ 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_ssl, plugin); return FALSE; }