/* GKrellM | Copyright (C) 1999-2007 Bill Wilson | | Author: Bill Wilson billw@gkrellm.net | Latest versions might be found at: http://gkrellm.net | | | GKrellM 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, either version 3 of the License, or | (at your option) any later version. | | GKrellM 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 http://www.gnu.org/licenses/ */ #include "gkrellmd.h" #include "gkrellmd-private.h" #if !defined(WIN32) #define MBOX_MBOX 0 #define MBOX_MAILDIR 1 #define MBOX_MH_DIR 2 typedef struct { gchar *path; gchar *homedir_path; gint mboxtype; gboolean (*check_func)(); gint mail_count; gint new_mail_count; gint old_mail_count; gint prev_mail_count, prev_new_mail_count; time_t last_mtime; off_t last_size; gboolean is_internal; /* Internal mail message (ie: localmachine) */ gboolean changed; } Mailbox; static GList *mailbox_list; static gint mail_check_timeout = 5; /* Seconds */ static gboolean unseen_is_new = TRUE; /* Accessed but unread */ static gboolean mail_need_serve; /* Look at a From line to see if it is valid, lines look like: | From sending_address dayofweek month dayofmonth timeofday year | eg: From billw@gkrellm.net Fri Oct 22 13:52:49 2010 */ static gint is_From_line(Mailbox *mbox, gchar *buf) { gchar sender[512]; gint dayofmonth = 0; if (strncmp(buf, "From ", 5)) return FALSE; /* In case sending address missing, look for a day of month | number in field 3 or 4 (0 based). */ sender[0] = '\0'; if (sscanf(buf, "%*s %*s %*s %d", &dayofmonth) != 1) { if (sscanf(buf, "%*s %511s %*s %*s %d", sender, &dayofmonth) != 2) return FALSE; } if (dayofmonth < 1 || dayofmonth > 31) return FALSE; if (strcmp(sender, "MAILER-DAEMON") == 0) mbox->is_internal = TRUE; return TRUE; } /* Check if this is a Content-Type-line. If it contains a boundary | field, copy boundary string to buffer (including two leading and | trailing dashes marking the end of a multipart mail) and return | true. Otherwise, return false. */ static gint is_multipart_mail(gchar *buf, gchar *separator) { gchar *fieldstart; gchar *sepstart; gint seplen; if (strncmp(buf, "Content-Type: ", 14) != 0) return FALSE; if (strncmp(&buf[14], "multipart/", 10) != 0) return FALSE; fieldstart = &buf[14]; while (*fieldstart!=0) { while (*fieldstart!=0 && *fieldstart!=';') fieldstart++; if (*fieldstart==';') fieldstart++; while (*fieldstart!=0 && *fieldstart==' ') fieldstart++; if (strncmp(fieldstart, "boundary=", 9) == 0) { sepstart = fieldstart + 9; if (sepstart[0]=='"') { sepstart++; seplen = 0; while (sepstart[seplen]!='"' && sepstart[seplen]>=32) seplen++; } else { seplen = 0; while (sepstart[seplen]!=';' && sepstart[seplen]>32) seplen++; } strcpy(separator,"--"); strncpy(&separator[2],sepstart,seplen); strcpy(&separator[seplen+2],"--"); return TRUE; } } return FALSE; } static gboolean mh_sequences_new_count(Mailbox *mbox) { FILE *f; gchar buf[1024]; gchar *path, *tok; gint n0, n1; path = g_strconcat(mbox->path, G_DIR_SEPARATOR_S, ".mh_sequences", NULL); f = fopen(path, "r"); g_free(path); if (!f) return FALSE; while (fgets(buf, sizeof(buf), f)) { /* Look for unseen sequence like "unseen: 4 7-9 23" */ if (strncmp(buf, "unseen:", 7)) continue; tok = strtok(buf, " \t\n"); while ((tok = strtok(NULL, " \t\n")) != NULL) { if (sscanf(tok, "%d-%d", &n0, &n1) == 2) mbox->new_mail_count += n1 - n0 + 1; else mbox->new_mail_count++; } break; } fclose(f); return TRUE; } /* Sylpheed procmsg.h enums MSG_NEW as (1 << 0) and MSG_UNREAD as (1 << 1) | And procmsg_write_flags() in Sylpheeds procmsg.c writes a mail record as | a pair of ints with msgnum first followed by flags. */ #define SYLPHEED_MSG_NEW 1 #define SYLPHEED_MSG_UNREAD 2 #define SYLPHEED_MARK_VERSION 2 static gboolean sylpheed_mark_new_count(Mailbox *mbox) { FILE *f; gchar *path; gint msgnum, flags, ver, mark_files = 0; path = g_strconcat(mbox->path, G_DIR_SEPARATOR_S, ".sylpheed_mark", NULL); f = fopen(path, "rb"); g_free(path); if (!f) return FALSE; if ( fread(&ver, sizeof(ver), 1, f) == 1 && SYLPHEED_MARK_VERSION == ver ) { while ( fread(&msgnum, sizeof(msgnum), 1, f) == 1 && fread(&flags, sizeof(flags), 1, f) == 1 ) { if ( (flags & SYLPHEED_MSG_NEW) || ((flags & SYLPHEED_MSG_UNREAD) && unseen_is_new) ) mbox->new_mail_count += 1; ++mark_files; } if (mark_files < mbox->mail_count) mbox->new_mail_count += mbox->mail_count - mark_files; } fclose(f); return TRUE; } /* Check a mh directory for mail. The way that messages are marked as new | depends on the MUA being using. Only .mh_sequences and .sylpheed_mark | are currently checked, otherwise all mail found is considered new mail. */ static gboolean check_mh_dir(Mailbox *mbox) { GDir *dir; gchar *name; mbox->mail_count = mbox->new_mail_count = 0; if ((dir = g_dir_open(mbox->path, 0, NULL)) == NULL) return FALSE; while ((name = (gchar *) g_dir_read_name(dir)) != NULL) { /* Files starting with a digit are messages. */ if (isdigit((unsigned char)name[0])) mbox->mail_count++; } g_dir_close(dir); /* Some MH dir clients use .mh_sequences, others such as mutt or gnus | do not. For mixed cases, it's a user option to ignore .mh_sequences. | Sylpheed uses .sylpheed_mark. */ if ( !mh_sequences_new_count(mbox) && !sylpheed_mark_new_count(mbox) ) mbox->new_mail_count = mbox->mail_count; return TRUE; } /* A maildir has new, cur, and tmp subdirectories. Any file in new | or cur that does not begin with a '.' is a mail message. It is | suggested that messages begin with the output of time() (9 digits) | but while mutt and qmail use this standard, procmail does not. | maildir(5) says: | It is a good idea for readers to skip all filenames in | new and cur starting with a dot. Other than this, | readers should not attempt to parse filenames. | So check_maildir() simply looks for files in new and cur. | But if unseen_is_new flag is set, look for ":2,*S" file suffix where | the 'S' indicates the mail is seen. | See http://cr.yp.to/proto/maildir.html */ static gboolean check_maildir(Mailbox *mbox) { gchar path[256], *s; gchar *name; GDir *dir; mbox->new_mail_count = 0; snprintf(path, sizeof(path), "%s%cnew", mbox->path, G_DIR_SEPARATOR); if ((dir = g_dir_open(path, 0, NULL)) != NULL) { while ((name = (gchar *) g_dir_read_name(dir)) != NULL) mbox->new_mail_count++; g_dir_close(dir); } mbox->mail_count = mbox->new_mail_count; snprintf(path, sizeof(path), "%s%ccur", mbox->path, G_DIR_SEPARATOR); if ((dir = g_dir_open(path, 0, NULL)) != NULL) { while ((name = (gchar *) g_dir_read_name(dir)) != NULL) { mbox->mail_count++; if ( unseen_is_new && ( (s = strchr(name, ':')) == NULL || !strchr(s, 'S') ) ) mbox->new_mail_count++; } g_dir_close(dir); } if (_GK.debug_level & DEBUG_MAIL) g_print(_("mdir %s total=%d old=%d new=%d\n"), mbox->path, mbox->mail_count, mbox->old_mail_count, mbox->new_mail_count); return TRUE; } /* Count total mail and old mail in a mailbox. Old mail can be read | with a Status: R0, or can be accessed and not read with Status: O | So, new mail will be the diff - note that unread mail is not | necessarily new mail. According to stat() man page: | st_atime is changed by mknod(), utime(), read(), write(), truncate() | st_mtime is changed by mknod(), utime(), write() | But, new mail arriving (writing mailbox) sets st_mtime while reading | the mailbox (mail program reading) sets st_atime. So the test | st_atime > st_mtime is testing if mbox has been read since last new mail. | Mail readers may restore st_mtime after writting status. | And Netscape mail does status with X-Mozilla-Status: xxxS | where S is bitwise or of status flags: | 1: read 2: replied 4: marked 8: deleted | | Evolution uses status with X-Evolution: 00000000-xxxx where xxxx status is | a bitfield in hexadecimal (see enum _CamelMessageFlags in evolution/camel | source) and most importantly CAMEL_MESSAGE_SEEN = 1<<4. */ /* test if buf is a status for standard mail, mozilla or evolution */ static gboolean is_status(gchar *buf) { if (buf[0] != 'S' && buf[0] != 'X') return FALSE; if ( !strncmp(buf, "Status:", 7) /* Standard mail clients */ || !strncmp(buf, "X-Mozilla-Status:", 17) /* Netscape */ || !strncmp(buf, "X-Evolution:", 12) /* Mozilla */ ) return TRUE; else return FALSE; } static gboolean status_is_old(gchar *buf) { gchar c; int tmp; /* Standard mail clients */ if ( !strncmp(buf, "Status:", 7) && (strchr(buf, 'R') || (!unseen_is_new && strchr(buf, 'O'))) ) return TRUE; /* Netscape */ if (!strncmp(buf, "X-Mozilla-Status:", 17)) { c = buf[21]; if (c < '8') /* Not deleted */ c -= '0'; if (c >= '8' || (c & 0x1)) return TRUE; } /* Evolution */ if (!strncmp(buf, "X-Evolution:", 12)) { sscanf(buf+22, "%04x", &tmp); if (tmp & (1<<4)) return TRUE; } return FALSE; } /* test if a mail is marked as deleted | Evolution uses status with X-Evolution: 00000000-xxxx where xxxx status is | a bitfield in hexadecimal (see enum _CamelMessageFlags in evolution/camel source) | and most importantly CAMEL_MESSAGE_DELETED = 1<<1. */ static gboolean status_is_deleted(gchar *buf) { gint tmp; /* Standard mail clients if ( !strncmp(buf, "Status:", 7) ) */ /* Netscape if (!strncmp(buf, "X-Mozilla-Status:", 17)) */ /* Evolution */ if (!strncmp(buf, "X-Evolution:", 12)) { sscanf(buf+22, "%04x", &tmp); if (tmp & (1<<1)) return TRUE; /* Junk is not explicitly marked as deleted but is shown as if | where in evolution */ if (tmp & (1<<7)) return TRUE; } return FALSE; } static gboolean check_mbox(Mailbox *mbox) { FILE *f; struct utimbuf ut; struct stat s; gchar buf[1024]; gchar mpart_sep[1024]; gint in_header = FALSE; gint marked_read = FALSE; gint is_multipart = FALSE; if (stat(mbox->path, &s) != 0) { mbox->mail_count = mbox->old_mail_count = mbox->new_mail_count = 0; mbox->last_mtime = 0; mbox->last_size = 0; if (_GK.debug_level & DEBUG_MAIL) printf("check_mbox can't stat(%s): %s\n", mbox->path, g_strerror(errno)); return FALSE; } /* If the mailboxes have been modified since last check, count | the new/total messages. */ if ( s.st_mtime != mbox->last_mtime || s.st_size != mbox->last_size ) { if ((f = fopen(mbox->path, "r")) == NULL) { if (_GK.debug_level & DEBUG_MAIL) printf("check_mbox can't fopen(%s): %s\n", mbox->path, g_strerror(errno)); return FALSE; } mbox->mail_count = 0; mbox->old_mail_count = 0; while(fgets(buf, sizeof(buf), f)) { if (is_multipart && !in_header) { /* Skip to last line of multipart mail */ if (strncmp(buf,mpart_sep,strlen(mpart_sep))==0) is_multipart = FALSE; } else if (buf[0] == '\n') { in_header = FALSE; mbox->is_internal = FALSE; } else if (is_From_line(mbox, buf)) { mbox->mail_count += 1; in_header = TRUE; marked_read = FALSE; } else if (in_header && is_status(buf)) { if (status_is_old(buf) && !marked_read) { mbox->old_mail_count += 1; marked_read = TRUE; } if (status_is_deleted(buf)) { if (marked_read) mbox->old_mail_count -= 1; mbox->mail_count -= 1; } } else if (in_header && mbox->is_internal) { if (strncmp(buf, "From: Mail System Internal Data", 31) == 0) { in_header = FALSE; mbox->mail_count -= 1; mbox->is_internal = FALSE; } } else if (in_header && is_multipart_mail(buf,mpart_sep)) { is_multipart = TRUE; } } fclose(f); /* Restore the mbox stat times for other mail checking programs and | so the (st_atime > st_mtime) animation override below will work. */ ut.actime = s.st_atime; ut.modtime = s.st_mtime; utime(mbox->path, &ut); mbox->last_mtime = s.st_mtime; mbox->last_size = s.st_size; if (_GK.debug_level & DEBUG_MAIL) g_print("mbox read <%s> total=%d old=%d\n", mbox->path, mbox->mail_count, mbox->old_mail_count); } /* If mbox has been accessed since last modify a MUA has probably read | the mbox. */ mbox->new_mail_count = mbox->mail_count - mbox->old_mail_count; if (s.st_atime > s.st_mtime) { mbox->prev_new_mail_count = mbox->new_mail_count; } return TRUE; } static void update_mail(GkrellmdMonitor *mon, gboolean force) { Mailbox *mbox; GList *list; static gint second_count; if ( (!GK.second_tick || (++second_count % mail_check_timeout) != 0) && !force ) return; for (list = mailbox_list; list; list = list->next) { mbox = (Mailbox *) list->data; if (mbox->check_func) (*mbox->check_func)(mbox); if ( mbox->prev_mail_count != mbox->mail_count || mbox->prev_new_mail_count != mbox->new_mail_count ) { mbox->changed = TRUE; mail_need_serve = TRUE; gkrellmd_need_serve(mon); } mbox->prev_mail_count = mbox->mail_count; mbox->prev_new_mail_count = mbox->new_mail_count; } } static void get_local_mboxtype(Mailbox *mbox) { gchar *path; if (*(mbox->path) == '~') { mbox->homedir_path = mbox->path; mbox->path = g_strdup_printf("%s%s", g_get_home_dir(), mbox->homedir_path + 1); } if (g_file_test(mbox->path, G_FILE_TEST_IS_DIR)) { #if GLIB_CHECK_VERSION(2,0,0) path = g_build_path(G_DIR_SEPARATOR_S, mbox->path, "new", NULL); #else path = g_strconcat(mbox->path, G_DIR_SEPARATOR_S, "new", NULL); #endif if (g_file_test(path, G_FILE_TEST_IS_DIR)) mbox->mboxtype = MBOX_MAILDIR; else mbox->mboxtype = MBOX_MH_DIR; g_free(path); } else mbox->mboxtype = MBOX_MBOX; } void gkrellmd_add_mailbox(gchar *path) { Mailbox *mbox; if (!path || !*path) return; mbox = g_new0(Mailbox, 1); mbox->path = g_strdup(path); get_local_mboxtype(mbox); if (mbox->mboxtype == MBOX_MAILDIR) mbox->check_func = check_maildir; else if (mbox->mboxtype == MBOX_MH_DIR) mbox->check_func = check_mh_dir; else mbox->check_func = check_mbox; mailbox_list = g_list_append(mailbox_list, mbox); gkrellmd_add_serveflag_done(&mbox->changed); } /* ============================================================= */ static void serve_mail_data(GkrellmdMonitor *mon, gboolean first_serve) { Mailbox *mbox; GList *list; gchar *line; if ((!mail_need_serve && !first_serve) || !mailbox_list) return; gkrellmd_set_serve_name(mon, "mail"); for (list = mailbox_list; list; list = list->next) { mbox = (Mailbox *) list->data; if (mbox->changed || first_serve) { line = g_strdup_printf("%s %d %d\n", mbox->homedir_path ? mbox->homedir_path : mbox->path, mbox->mail_count, mbox->new_mail_count); gkrellmd_serve_data(mon, line); g_free(line); } } } static void serve_mail_setup(GkrellmdMonitor *mon) { GkrellmdClient *client = mon->privat->client; GList *list; Mailbox *mbox; gchar *line; gkrellmd_send_to_client(client, "\n"); for (list = mailbox_list; list; list = list->next) { mbox = (Mailbox *) list->data; line = g_strdup_printf("%s\n", mbox->homedir_path ? mbox->homedir_path : mbox->path); gkrellmd_send_to_client(client, line); g_free(line); } } static GkrellmdMonitor mail_monitor = { "mail", update_mail, serve_mail_data, serve_mail_setup }; GkrellmdMonitor * gkrellmd_init_mail_monitor(void) { gkrellmd_add_serveflag_done(&mail_need_serve); return &mail_monitor; } #else /* defined(WIN32) */ GkrellmdMonitor * gkrellmd_init_mail_monitor(void) { return NULL; } void gkrellmd_add_mailbox(gchar *path) { } #endif void gkrellm_mail_local_unsupported(void) { /* WIN32 only calls this and it is taken care of by above #if */ } #if GLIB_CHECK_VERSION(2,0,0) GThread * #else gpointer #endif gkrellm_mail_get_active_thread(void) { return NULL; }