/* message.c - Utility functions for parsing an email message for notmuch. * * Copyright © 2009 Carl Worth * * 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, either version 3 of the License, or * (at your option) any later version. * * 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 http://www.gnu.org/licenses/ . * * Author: Carl Worth */ #include #include "notmuch-private.h" #include #include /* GHashTable */ struct _notmuch_message_file { /* File object */ FILE *file; char *filename; /* Header storage */ int restrict_headers; GHashTable *headers; GMimeStream *stream; GMimeParser *parser; GMimeMessage *message; notmuch_bool_t parsed; }; static int strcase_equal (const void *a, const void *b) { return strcasecmp (a, b) == 0; } static unsigned int strcase_hash (const void *ptr) { const char *s = ptr; /* This is the djb2 hash. */ unsigned int hash = 5381; while (s && *s) { hash = ((hash << 5) + hash) + tolower (*s); s++; } return hash; } static int _notmuch_message_file_destructor (notmuch_message_file_t *message) { if (message->headers) g_hash_table_destroy (message->headers); if (message->message) g_object_unref (message->message); if (message->parser) g_object_unref (message->parser); if (message->stream) g_object_unref (message->stream); else if (message->file) /* GMimeStream takes over the FILE* */ fclose (message->file); return 0; } /* Create a new notmuch_message_file_t for 'filename' with 'ctx' as * the talloc owner. */ notmuch_message_file_t * _notmuch_message_file_open_ctx (void *ctx, const char *filename) { notmuch_message_file_t *message; message = talloc_zero (ctx, notmuch_message_file_t); if (unlikely (message == NULL)) return NULL; /* only needed for error messages during parsing */ message->filename = talloc_strdup (message, filename); if (message->filename == NULL) goto FAIL; talloc_set_destructor (message, _notmuch_message_file_destructor); message->file = fopen (filename, "r"); if (message->file == NULL) goto FAIL; message->headers = g_hash_table_new_full (strcase_hash, strcase_equal, free, g_free); if (message->headers == NULL) goto FAIL; return message; FAIL: fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); notmuch_message_file_close (message); return NULL; } notmuch_message_file_t * notmuch_message_file_open (const char *filename) { return _notmuch_message_file_open_ctx (NULL, filename); } void notmuch_message_file_close (notmuch_message_file_t *message) { talloc_free (message); } void notmuch_message_file_restrict_headersv (notmuch_message_file_t *message, va_list va_headers) { char *header; if (message->parsed) INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started"); while (1) { header = va_arg (va_headers, char*); if (header == NULL) break; g_hash_table_insert (message->headers, xstrdup (header), NULL); } message->restrict_headers = 1; } void notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...) { va_list va_headers; va_start (va_headers, message); notmuch_message_file_restrict_headersv (message, va_headers); } /* * gmime does not provide access to all Received: headers the way we * want, so we'll to use the parser header callback to gather them * into a hash table. */ static void header_cb (unused(GMimeParser *parser), const char *header, const char *value, unused(gint64 offset), gpointer user_data) { notmuch_message_file_t *message = (notmuch_message_file_t *) user_data; char *existing = NULL; notmuch_bool_t found; found = g_hash_table_lookup_extended (message->headers, header, NULL, (gpointer *) &existing); if (! found && message->restrict_headers) return; if (existing == NULL) { /* No value, add one */ char *decoded = g_mime_utils_header_decode_text (value); g_hash_table_insert (message->headers, xstrdup (header), decoded); } else if (strcasecmp (header, "received") == 0) { /* Concat all of the Received: headers we encounter. */ char *combined, *decoded; size_t combined_size; decoded = g_mime_utils_header_decode_text (value); combined_size = strlen(existing) + strlen(decoded) + 2; combined = g_malloc (combined_size); snprintf (combined, combined_size, "%s %s", existing, decoded); g_free (decoded); g_hash_table_insert (message->headers, xstrdup (header), combined); } } notmuch_status_t notmuch_message_file_parse (notmuch_message_file_t *message) { static int initialized = 0; char from_buf[5]; notmuch_bool_t is_mbox = FALSE; static notmuch_bool_t mbox_warning = FALSE; if (! initialized) { g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS); initialized = 1; } /* Is this mbox? */ if (fread (from_buf, sizeof (from_buf), 1, message->file) == 1 && strncmp (from_buf, "From ", 5) == 0) is_mbox = TRUE; rewind (message->file); message->stream = g_mime_stream_file_new (message->file); message->parser = g_mime_parser_new_with_stream (message->stream); g_mime_parser_set_scan_from (message->parser, is_mbox); g_mime_parser_set_header_regex (message->parser, ".*", header_cb, (gpointer) message); message->message = g_mime_parser_construct_message (message->parser); if (! message->message) return NOTMUCH_STATUS_FILE_NOT_EMAIL; if (is_mbox) { if (! g_mime_parser_eos (message->parser)) { /* This is a multi-message mbox. */ return NOTMUCH_STATUS_FILE_NOT_EMAIL; } /* * For historical reasons, we support single-message mboxes, * but this behavior is likely to change in the future, so * warn. */ if (! mbox_warning) { mbox_warning = TRUE; fprintf (stderr, "\ Warning: %s is an mbox containing a single message,\n\ likely caused by misconfigured mail delivery. Support for single-message\n\ mboxes is deprecated and may be removed in the future.\n", message->filename); } } message->parsed = TRUE; return NOTMUCH_STATUS_SUCCESS; } /* return NULL on errors, empty string for non-existing headers */ const char * notmuch_message_file_get_header (notmuch_message_file_t *message, const char *header) { const char *value = NULL; notmuch_bool_t found; /* the caller shouldn't ask for headers before parsing */ if (! message->parsed) return NULL; found = g_hash_table_lookup_extended (message->headers, header, NULL, (gpointer *) &value); /* the caller shouldn't ask for non-restricted headers */ if (! found && message->restrict_headers) return NULL; return value ? value : ""; }