/* $Id: mbox.c,v 1.27 2011/02/21 01:30: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 . */ /* TODO: * - import mails automatically from the spool to the inbox */ #include #include #include #include #include #include #include "Mailer.h" #define min(a, b) ((a) < (b) ? (a) : (b)) /* Mbox */ /* private */ #define _FOLDER_CNT 5 /* types */ typedef enum _ParserContext { PC_FROM, /* while pos < 6 not sure => fallback body or garbage */ PC_HEADER, /* inside a header */ PC_BODY, /* inside a body */ PC_GARBAGE /* inside crap */ } ParserContext; typedef struct _Mbox Mbox; typedef struct _MboxFolder { Mbox * mbox; AccountFolder folder; AccountConfig * config; AccountMessage ** messages; size_t messages_cnt; GtkTextBuffer * buffer; /* refresh */ time_t mtime; GIOChannel * channel; int source; /* parsing */ size_t offset; ParserContext context; AccountMessage * message; size_t pos; /* context-dependant */ char * str; /* interface */ char * pixbuf; } MboxFolder; struct _Mbox { AccountPlugin * plugin; MboxFolder folders[_FOLDER_CNT]; /* refresh */ unsigned int timeout; }; /* constants */ #define MBOX_REFRESH_TIMEOUT 5000 /* variables */ static AccountConfig _mbox_config[_FOLDER_CNT + 1] = { { "mbox", "Inbox file", ACT_FILE, NULL }, { "spool", "Spool file", ACT_FILE, NULL }, { "draft", "Draft mails file", ACT_FILE, NULL }, { "sent", "Sent mails file", ACT_FILE, NULL }, { "trash", "Deleted mails file", ACT_FILE, NULL }, { NULL, NULL, 0, NULL } }; static const MboxFolder _mbox_folder_defaults[_FOLDER_CNT] = { { NULL, { AFT_INBOX, "Inbox", NULL, NULL }, &_mbox_config[0], NULL, 0, NULL, 0, NULL, -1, 0, PC_FROM, NULL, 0, NULL, "mailer-inbox" }, { NULL, { AFT_INBOX, "Spool", NULL, NULL }, &_mbox_config[1], NULL, 0, NULL, 0, NULL, -1, 0, PC_FROM, NULL, 0, NULL, "mailer-inbox" }, { NULL, { AFT_DRAFTS, "Drafts", NULL, NULL }, &_mbox_config[2], NULL, 0, NULL, 0, NULL, -1, 0, PC_FROM, NULL, 0, NULL, "stock_mail-handling" }, { NULL, { AFT_SENT, "Sent", NULL, NULL }, &_mbox_config[3], NULL, 0, NULL, 0, NULL, -1, 0, PC_FROM, NULL, 0, NULL, "mailer-sent" }, { NULL, { AFT_TRASH, "Trash", NULL, NULL }, &_mbox_config[4], NULL, 0, NULL, 0, NULL, -1, 0, PC_FROM, NULL, 0, NULL, "stock_trash_full" } }; /* plug-in */ static int _mbox_init(AccountPlugin * plugin, GtkTreeStore * store, GtkTreeIter * parent, GtkTextBuffer * buffer); static int _mbox_destroy(AccountPlugin * plugin); static GtkTextBuffer * _mbox_select(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message); static GtkTextBuffer * _mbox_select_source(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message); AccountPlugin account_plugin = { NULL, "MBOX", "Local folders", NULL, _mbox_config, _mbox_init, _mbox_destroy, _mbox_select, _mbox_select_source, NULL }; /* prototypes */ /* callbacks */ static gboolean _folder_idle(gpointer data); static gboolean _folder_watch(GIOChannel * source, GIOCondition condition, gpointer data); /* AccountMessage */ /* private */ /* types */ struct _AccountMessage { size_t offset; GtkTreeIter iter; char ** headers; size_t headers_cnt; size_t body_offset; size_t body_length; }; /* prototypes */ static AccountMessage * _message_new(off_t offset, GtkListStore * store); static void _message_delete(AccountMessage * message); static int _message_set_body(AccountMessage * message, off_t offset, size_t length); static int _message_set_header(AccountMessage * message, char const * header, GtkListStore * store); /* Mbox */ /* functions */ /* mbox_init */ static int _mbox_init(AccountPlugin * plugin, GtkTreeStore * store, GtkTreeIter * parent, GtkTextBuffer * buffer) { int ret = 0; Mbox * mbox; size_t i; char * filename; AccountFolder * af; MboxFolder * folder; GdkPixbuf * pixbuf; GtkTreeIter iter; #ifdef DEBUG fprintf(stderr, "DEBUG: %s()\n", __func__); #endif if((mbox = malloc(sizeof(*mbox))) == NULL) return -1; plugin->priv = mbox; mbox->plugin = plugin; memcpy(mbox->folders, _mbox_folder_defaults, sizeof(mbox->folders)); mbox->timeout = MBOX_REFRESH_TIMEOUT; for(i = 0; i < _FOLDER_CNT; i++) { af = &mbox->folders[i].folder; af->data = &mbox->folders[i]; folder = &mbox->folders[i]; folder->mbox = mbox; folder->buffer = buffer; filename = folder->config->value; if(filename == NULL) continue; pixbuf = gtk_icon_theme_load_icon(account_plugin.helper->theme, (folder->pixbuf != NULL) ? folder->pixbuf : "stock_folder", 16, 0, NULL); gtk_tree_store_append(store, &iter, parent); gtk_tree_store_set(store, &iter, MF_COL_ACCOUNT, NULL, MF_COL_FOLDER, af, MF_COL_ICON, pixbuf, MF_COL_NAME, af->name, -1); g_object_unref(pixbuf); /* XXX should not be done here? */ af->store = gtk_list_store_new(MH_COL_COUNT, G_TYPE_POINTER, G_TYPE_POINTER, G_TYPE_POINTER, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT); folder->source = g_idle_add(_folder_idle, af); } return ret; } /* mbox_destroy */ static int _mbox_destroy(AccountPlugin * plugin) { Mbox * mbox = plugin->priv; size_t i; MboxFolder * mf; size_t j; if(mbox == NULL) /* XXX _mbox_destroy() may be called uninitialized */ return 0; for(i = 0; i < _FOLDER_CNT; i++) { mf = &mbox->folders[i]; for(j = 0; j < mf->messages_cnt; j++) _message_delete(mf->messages[j]); free(mf->messages); mf->messages = NULL; mf->messages_cnt = 0; } free(mbox); return 0; } /* mbox_select */ static GtkTextBuffer * _mbox_select(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message) { MboxFolder * mf = folder->data; char const * filename = mf->config->value; GtkTextIter iter; FILE * fp; char * buf; size_t size; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\", \"%p\")\n", __func__, folder->name, (void*)message); #endif gtk_text_buffer_set_text(mf->buffer, "", 0); gtk_text_buffer_get_end_iter(mf->buffer, &iter); /* XXX we may still be reading the file... */ if((fp = fopen(filename, "r")) == NULL) { plugin->helper->error(NULL, strerror(errno), 1); return NULL; } if(message->body_offset != 0 && message->body_length > 0 && fseek(fp, message->body_offset, SEEK_SET) == 0 && (buf = malloc(message->body_length)) != NULL) { if((size = fread(buf, 1, message->body_length, fp)) > 0) gtk_text_buffer_insert(mf->buffer, &iter, buf, size); free(buf); } fclose(fp); return mf->buffer; } /* mbox_select_source */ static GtkTextBuffer * _mbox_select_source(AccountPlugin * plugin, AccountFolder * folder, AccountMessage * message) { /* FIXME code duplication with _mbox_select */ GtkTextBuffer * ret; MboxFolder * mf = folder->data; char const * filename = mf->config->value; GtkTextIter iter; FILE * fp; char * buf; size_t size; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\", \"%p\")\n", __func__, folder->name, (void*)message); #endif ret = gtk_text_buffer_new(NULL); gtk_text_buffer_get_end_iter(ret, &iter); /* XXX we may still be reading the file... */ if((fp = fopen(filename, "r")) == NULL) { plugin->helper->error(NULL, strerror(errno), 1); return NULL; } size = message->body_offset - message->offset + message->body_length; if(fseek(fp, message->offset, SEEK_SET) == 0 && (buf = malloc(size)) != NULL) { if((size = fread(buf, 1, size, fp)) > 0) gtk_text_buffer_insert(ret, &iter, buf, size); free(buf); } fclose(fp); return ret; } /* AccountMessage */ /* functions */ /* message_new */ static AccountMessage * _message_new(off_t offset, GtkListStore * store) { AccountMessage * message; if((message = malloc(sizeof(*message))) == NULL) { /* FIXME catch error */ return NULL; } message->offset = offset; gtk_list_store_append(store, &message->iter); gtk_list_store_set(store, &message->iter, MH_COL_MESSAGE, message, MH_COL_PIXBUF, account_plugin.helper->mail_read, -1); message->headers = NULL; message->headers_cnt = 0; message->body_offset = 0; message->body_length = 0; return message; } /* message_delete */ static void _message_delete(AccountMessage * message) { size_t i; for(i = 0; i < message->headers_cnt; i++) free(message->headers[i]); free(message->headers); free(message); } /* message_set_body */ static int _message_set_body(AccountMessage * message, off_t offset, size_t length) { #ifdef DEBUG fprintf(stderr, "DEBUG: %s(%p, %lu, %lu)\n", __func__, (void*)message, offset, length); #endif message->body_offset = offset; message->body_length = length; return 0; } /* message_set_header */ /* FIXME factorize code? */ static int _message_set_header(AccountMessage * message, char const * header, GtkListStore * store) { /* FIXME check if the header is already set */ char ** p; struct { int col; char const * name; } abc[] = { { MH_COL_SUBJECT, "Subject: " }, { MH_COL_FROM, "From: " }, { MH_COL_FROM, "From " }, { MH_COL_TO, "To: " }, { MH_COL_DATE_DISPLAY, "Date: " }, { MH_COL_READ, "Status: " }, { -1, NULL } }; size_t i; struct tm t; time_t oneday; char buf[20]; gboolean read; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(%p, \"%s\", store)\n", __func__, (void*)message, header); #endif if((p = realloc(message->headers, sizeof(*p) * (message->headers_cnt + 1))) == NULL) { /* FIXME catch error */ return -1; } message->headers = p; if((message->headers[message->headers_cnt] = strdup(header)) == NULL) return -1; message->headers_cnt++; for(i = 0; abc[i].col != -1; i++) if(strncmp(header, abc[i].name, strlen(abc[i].name)) == 0) break; if(abc[i].col == MH_COL_DATE_DISPLAY) { oneday = time(NULL) - 86400; memset(&t, 0, sizeof(t)); if(strptime(&header[6], "%a, %d %b %Y %H:%M:%S", &t) != NULL) strftime(buf, sizeof(buf), mktime(&t) > oneday ? "Today %X" : "%x %X", &t); else { #ifdef DEBUG fprintf(stderr, "DEBUG: %s() \"%s\" failed\n", __func__, &header[6]); #endif snprintf(buf, sizeof(buf), "%s", &header[6]); } gtk_list_store_set(store, &message->iter, MH_COL_DATE, mktime(&t), MH_COL_DATE_DISPLAY, buf, -1); } else if(abc[i].col == MH_COL_READ) { read = (index(&header[8], 'R') != NULL) ? TRUE : FALSE; gtk_list_store_set(store, &message->iter, MH_COL_READ, read, MH_COL_WEIGHT, read ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD, -1); gtk_list_store_set(store, &message->iter, MH_COL_PIXBUF, read ? account_plugin.helper->mail_read : account_plugin.helper->mail_unread, -1); } else if(abc[i].col != -1) gtk_list_store_set(store, &message->iter, abc[i].col, &header[strlen(abc[i].name)], -1); return 0; } AccountMessage * _folder_message_add(AccountFolder * folder, off_t offset) { MboxFolder * mbox = folder->data; AccountMessage ** p; AccountMessage * message; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\", %ld)\n", __func__, (char const *)mbox->config->value, offset); #endif if((p = realloc(mbox->messages, sizeof(*p) * (mbox->messages_cnt + 1))) == NULL) { /* FIXME track error */ return NULL; } mbox->messages = p; if((message = _message_new(offset, folder->store)) == NULL) { /* FIXME track error */ return NULL; } mbox->messages[mbox->messages_cnt++] = message; return message; } /* functions */ /* callbacks */ /* folder_idle */ static gboolean _folder_idle(gpointer data) { AccountFolder * folder = data; MboxFolder * mf = folder->data; Mbox * mbox = mf->mbox; struct stat st; char const * filename = mf->config->value; GError * error = NULL; #ifdef DEBUG fprintf(stderr, "DEBUG: %s() stat(\"%s\")\n", __func__, filename); #endif if(filename == NULL || filename[0] == '\0') return FALSE; if(stat(filename, &st) != 0) { mbox->plugin->helper->error(NULL, strerror(errno), 1); mf->source = g_timeout_add(mbox->timeout, _folder_idle, folder); return FALSE; } if(st.st_mtime == mf->mtime) { mf->source = g_timeout_add(mbox->timeout, _folder_idle, folder); return FALSE; } mf->mtime = st.st_mtime; /* FIXME only when done */ if(mf->channel == NULL) if((mf->channel = g_io_channel_new_file(filename, "r", &error)) == NULL) { mbox->plugin->helper->error(NULL, error->message, 1); mf->source = g_timeout_add(mbox->timeout, _folder_idle, folder); return FALSE; } g_io_channel_set_encoding(mf->channel, NULL, NULL); mf->source = g_io_add_watch(mf->channel, G_IO_IN, _folder_watch, folder); return FALSE; } /* folder_watch */ static void _watch_parse(AccountFolder * folder, char const buf[], size_t read); static int _parse_append(AccountFolder * folder, char const buf[], size_t len); static void _parse_from(AccountFolder * folder, char const buf[], size_t read, size_t * i); static void _parse_garbage(AccountFolder * folder, char const buf[], size_t read, size_t * i); static void _parse_header(AccountFolder * folder, char const buf[], size_t read, size_t * i); static void _parse_body(AccountFolder * folder, char const buf[], size_t read, size_t * i); static gboolean _folder_watch(GIOChannel * source, GIOCondition condition, gpointer data) { AccountFolder * folder = data; MboxFolder * mf = folder->data; Mbox * mbox = mf->mbox; char buf[BUFSIZ]; size_t read; GError * error = NULL; GIOStatus status; #ifdef DEBUG fprintf(stderr, "DEBUG: %s() \"%s\"\n", __func__, (char const *)mf->config->value); #endif if(condition != G_IO_IN) return FALSE; /* FIXME implement message deletion */ status = g_io_channel_read_chars(source, buf, sizeof(buf), &read, &error); switch(status) { case G_IO_STATUS_ERROR: mbox->plugin->helper->error(NULL, error->message, 1); /* FIXME new timeout 1000 function after invalidating * mtime */ return FALSE; case G_IO_STATUS_AGAIN: return TRUE; /* should not happen */ case G_IO_STATUS_EOF: case G_IO_STATUS_NORMAL: break; } _watch_parse(folder, buf, read); if(status == G_IO_STATUS_EOF) { /* XXX should not be necessary here */ if(mf->message != NULL) _message_set_body(mf->message, mf->message->body_offset, mf->offset - mf->message->body_offset); g_io_channel_close(source); mf->channel = NULL; mf->source = g_timeout_add(mbox->timeout, _folder_idle, folder); return FALSE; } return TRUE; } static void _watch_parse(AccountFolder * folder, char const buf[], size_t read) { MboxFolder * mbox = folder->data; size_t i = 0; while(i < read) switch(mbox->context) { case PC_FROM: _parse_from(folder, buf, read, &i); break; case PC_GARBAGE: _parse_garbage(folder, buf, read, &i); break; case PC_HEADER: _parse_header(folder, buf, read, &i); break; case PC_BODY: _parse_body(folder, buf, read, &i); break; } mbox->offset += read; } static int _parse_append(AccountFolder * folder, char const buf[], size_t len) { MboxFolder * mbox = folder->data; char * p; if((p = realloc(mbox->str, mbox->pos + len + 1)) == NULL) return -1; /* FIXME track error */ mbox->str = p; memcpy(&mbox->str[mbox->pos], buf, len); mbox->pos += len; mbox->str[mbox->pos] = '\0'; return 0; } static void _parse_context(AccountFolder * folder, ParserContext context) { MboxFolder * mbox = folder->data; mbox->context = context; free(mbox->str); mbox->str = NULL; mbox->pos = 0; } static void _parse_from(AccountFolder * folder, char const buf[], size_t read, size_t * i) { static char const from[] = "From "; MboxFolder * mbox = folder->data; size_t m; for(m = 0; *i + m < read && m < sizeof(from) - 1 && buf[*i + m] != '\n'; m++); _parse_append(folder, &buf[*i], m); *i += m; if(*i == read) /* not enough data read */ return; if(mbox->pos < sizeof(from) - 1 /* early newline */ || strncmp(mbox->str, from, sizeof(from) - 1) != 0) { if(mbox->message != NULL) mbox->context = PC_BODY; else mbox->context = PC_GARBAGE; return; /* switch context immediately */ } for(m = 0; *i + m < read && buf[*i + m] != '\n'; m++); _parse_append(folder, &buf[*i], m); *i += m; if(*i == read) return; /* grab more data XXX is gonna force a check again */ if(mbox->message != NULL) _message_set_body(mbox->message, mbox->message->body_offset, mbox->offset + *i - mbox->pos - mbox->message->body_offset); mbox->message = _folder_message_add(folder, mbox->offset + *i - mbox->pos); _message_set_header(mbox->message, mbox->str, folder->store); _parse_context(folder, PC_HEADER); /* read headers */ (*i)++; } static void _parse_garbage(AccountFolder * folder, char const buf[], size_t read, size_t * i) { for(; *i < read && buf[*i] != '\n'; (*i)++); if(*i == read) return; (*i)++; _parse_context(folder, PC_FROM); } static void _parse_header(AccountFolder * folder, char const buf[], size_t read, size_t * i) { MboxFolder * mbox = folder->data; size_t j; for(j = *i; j < read && buf[j] != '\n'; j++); _parse_append(folder, &buf[*i], j - *i); *i = j; if(j == read) return; _message_set_header(mbox->message, mbox->str, folder->store); _parse_context(folder, (mbox->pos == 0) ? PC_FROM : PC_HEADER); *i = ++j; } static void _parse_body(AccountFolder * folder, char const buf[], size_t read, size_t * i) { MboxFolder * mbox = folder->data; size_t j; for(j = *i; j < read && buf[j] != '\n'; j++); if(mbox->message->body_offset == 0) _message_set_body(mbox->message, mbox->offset + *i - mbox->pos, 0); _parse_append(folder, &buf[*i], j - *i); if(j == read) { /* TODO skip data instead of storing it */ *i = j; return; } _parse_context(folder, PC_FROM); *i = (j + 1); }