/* Copyright (c) 2004-2009 NFG Net Facilities Group BV support@nfg.nl 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 2 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, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /** * \file dbmail-message.c * * implements DbmailMessage object */ #include "dbmail.h" extern db_param_t _db_params; #define DBPFX _db_params.pfx #define DBMAIL_TEMPMBOX "INBOX" #define THIS_MODULE "message" /* * used for debugging message de/re-construction */ //#define dprint(fmt, args...) printf(fmt, ##args) #define dprint(fmt, args...) 0 /* * _register_header * * register a message header in a ghashtable dictionary * */ static void _register_header(const char *header, const char *value, gpointer user_data); static gboolean _header_cache(const char *header, const char *value, gpointer user_data); static DbmailMessage * _retrieve(DbmailMessage *self, const char *query_template); static void _map_headers(DbmailMessage *self); static int _set_content(DbmailMessage *self, const GString *content); static int _set_content_from_stream(DbmailMessage *self, GMimeStream *stream, dbmail_stream_t type); static int _message_insert(DbmailMessage *self, u64_t user_idnr, const char *mailbox, const char *unique_id); /* general mime utils (missing from gmime?) */ unsigned find_end_of_header(const char *h) { gchar c, p1 = 0, p2 = 0; unsigned i = 0; size_t l; assert(h); l = strlen(h); while (h++ && i<=l) { i++; c = *h; if (c == '\n' && ((p1 == '\n') || (p1 == '\r' && p2 == '\n'))) { if (l > i) i++; break; } p2 = p1; p1 = c; } return i; } gchar * g_mime_object_get_body(const GMimeObject *object) { gchar *s = NULL, *b = NULL; unsigned i; size_t l; g_return_val_if_fail(object != NULL, NULL); s = g_mime_object_to_string(GMIME_OBJECT(object)); assert(s); i = find_end_of_header(s); if (i >= strlen(s)) { g_free(s); return NULL; } b = s+i; l = strlen(b); memmove(s,b,l); s[l] = '\0'; s = g_realloc(s, l+1); return s; } gchar *get_crlf_encoded_opt(const char *in, int dots) { char prev = 0, curr = 0, *t, *out; const char *p = in; int i=0, nl = 0; assert(in); while (*p != '\0') { if ISLF(*p) nl++; p++; i++; } out = g_new0(char,i+(2*nl)+1); t = out; p = in; while (*p != '\0') { curr = *p; if ISLF(curr) { if (! ISCR(prev)) *t++ = '\r'; } if (dots && ISDOT(curr)) { if ISLF(prev) *t++ = '.'; } *t++=curr; prev = curr; p++; } return out; } static u64_t blob_exists(const char *buf, const char *hash) { volatile u64_t id = 0; size_t l; assert(buf); C c; S s; R r; l = strlen(buf); c = db_con_get(); TRY s = db_stmt_prepare(c,"SELECT id FROM %smimeparts WHERE hash=? AND size=? AND data=?", DBPFX); db_stmt_set_str(s,1,hash); db_stmt_set_u64(s,2,l); db_stmt_set_blob(s,3,buf,l); r = db_stmt_query(s); if (db_result_next(r)) id = db_result_get_u64(r,0); CATCH(SQLException) LOG_SQLERROR; FINALLY db_con_close(c); END_TRY; return id; } static u64_t blob_insert(const char *buf, const char *hash) { C c; R r; S s; size_t l; volatile u64_t id = 0; char *frag = db_returning("id"); assert(buf); l = strlen(buf); c = db_con_get(); TRY s = db_stmt_prepare(c, "INSERT INTO %smimeparts (hash, data, size) VALUES (?, ?, ?) %s", DBPFX, frag); db_stmt_set_str(s, 1, hash); db_stmt_set_blob(s, 2, buf, l); db_stmt_set_int(s, 3, l); r = db_stmt_query(s); id = db_insert_result(c,r); CATCH(SQLException) LOG_SQLERROR; FINALLY db_con_close(c); END_TRY; TRACE(TRACE_DEBUG,"inserted id [%llu]", id); g_free(frag); return id; } static int register_blob(DbmailMessage *m, u64_t id, gboolean is_header) { C c; volatile gboolean t = FALSE; c = db_con_get(); TRY t = db_exec(c, "INSERT INTO %spartlists (physmessage_id, is_header, part_key, part_depth, part_order, part_id) " "VALUES (%llu,%d,%d,%d,%d,%llu)", DBPFX, dbmail_message_get_physid(m), is_header, m->part_key, m->part_depth, m->part_order, id); CATCH(SQLException) LOG_SQLERROR; FINALLY db_con_close(c); END_TRY; return t; } static u64_t blob_store(const char *buf) { u64_t id; char *hash; if (! buf) return 0; hash = dm_get_hash_for_string(buf); if (! hash) return 0; // store this message fragment if ((id = blob_exists(buf, (const char *)hash)) != 0) { g_free(hash); return id; } if ((id = blob_insert(buf, (const char *)hash)) != 0) { g_free(hash); return id; } g_free(hash); return 0; } static int store_blob(DbmailMessage *m, const char *buf, gboolean is_header) { u64_t id; if (! buf) return 0; if (is_header) { m->part_key++; m->part_order=0; } dprint("<blob is_header=\"%d\" part_key=\"%d\" part_order=\"%d\">\n%s\n</blob>\n", is_header, m->part_key, m->part_order, buf); if (! (id = blob_store(buf))) return DM_EQUERY; // register this message fragment if (! register_blob(m, id, is_header)) return DM_EQUERY; m->part_order++; return 0; } static GMimeContentType *find_type(const char *s) { GMimeContentType *type; GString *header; char *rest, *h; int i=0; rest = g_strcasestr(s, "\nContent-type: "); if (! rest) { if ((g_strncasecmp(s, "Content-type: ", 14)) == 0) rest = (char *)s; } if (! rest) return NULL; header = g_string_new(""); i = 0; while (rest[i]) { if (rest[i] == ':') break; i++; } i++; while (rest[i]) { if (((rest[i] == '\n') || (rest[i] == '\r')) && (!isspace(rest[i+1]))) { break; } g_string_append_c(header,rest[i++]); } h = header->str; g_strstrip(h); type = g_mime_content_type_new_from_string(h); g_string_free(header,TRUE); return type; } static char * find_boundary(const char *s) { gchar *boundary; GMimeContentType *type = find_type(s); boundary = g_strdup(g_mime_content_type_get_parameter(type,"boundary")); g_mime_content_type_destroy(type); return boundary; } static DbmailMessage * _mime_retrieve(DbmailMessage *self) { C c; R r; char *str = NULL, *internal_date = NULL; char *boundary = NULL; GMimeContentType *mimetype = NULL; char **blist = g_new0(char *,128); int prevdepth, depth = 0, order, row = 0, key = 1; volatile int t = FALSE; gboolean got_boundary = FALSE, prev_boundary = FALSE, is_header = TRUE, prev_header, finalized=FALSE; gboolean prev_is_message = FALSE, is_message = FALSE; GString *m = NULL; const void *blob; field_t frag; assert(dbmail_message_get_physid(self)); date2char_str("ph.internal_date", &frag); c = db_con_get(); TRY r = db_query(c, "SELECT l.part_key,l.part_depth,l.part_order,l.is_header,%s,data " "FROM %smimeparts p " "JOIN %spartlists l ON p.id = l.part_id " "JOIN %sphysmessage ph ON ph.id = l.physmessage_id " "WHERE l.physmessage_id = %llu ORDER BY l.part_key,l.part_order ASC", frag, DBPFX, DBPFX, DBPFX, dbmail_message_get_physid(self)); m = g_string_new(""); row = 0; while (db_result_next(r)) { int l; prevdepth = depth; prev_header = is_header; key = db_result_get_int(r,0); depth = db_result_get_int(r,1); order = db_result_get_int(r,2); is_header = db_result_get_bool(r,3); if (row == 0) internal_date = g_strdup(db_result_get(r,4)); blob = db_result_get_blob(r,5,&l); str = g_new0(char,l+1); str = strncpy(str,blob,l); if (is_header) { prev_boundary = got_boundary; prev_is_message = is_message; mimetype = find_type(str); is_message = g_mime_content_type_is_type(mimetype, "message", "rfc822"); g_mime_content_type_destroy(mimetype); } got_boundary = FALSE; if (is_header && ((boundary = find_boundary(str)) != NULL)) { got_boundary = TRUE; dprint("<boundary depth=\"%d\">%s</boundary>\n", depth, boundary); if (blist[depth]) g_free(blist[depth]); blist[depth] = boundary; } if (prevdepth > depth && blist[depth]) { dprint("\n--%s at %d--\n", blist[depth], depth); g_string_append_printf(m, "\n--%s--\n", blist[depth]); g_free(blist[depth]); blist[depth] = NULL; finalized=TRUE; } if (depth>0 && blist[depth-1]) boundary = (char *)blist[depth-1]; if (is_header && (!prev_header || prev_boundary || (prev_header && depth>0 && !prev_is_message))) { dprint("\n--%s\n", boundary); g_string_append_printf(m, "\n--%s\n", boundary); } g_string_append(m, str); dprint("<part is_header=\"%d\" depth=\"%d\" key=\"%d\" order=\"%d\">\n%s\n</part>\n", is_header, depth, key, order, str); if (is_header) g_string_append_printf(m,"\n"); g_free(str); row++; } CATCH(SQLException) LOG_SQLERROR; t = DM_EQUERY; FINALLY db_con_close(c); END_TRY; if ((row == 0) || (t == DM_EQUERY)) return NULL; if (row > 2 && boundary && !finalized) { dprint("\n--%s-- final\n", boundary); g_string_append_printf(m, "\n--%s--\n", boundary); finalized=1; } if (row > 2 && depth > 0 && boundary && blist[0] && !finalized) { if (strcmp(blist[0],boundary)!=0) { dprint("\n--%s-- final\n", blist[0]); g_string_append_printf(m, "\n--%s--\n\n", blist[0]); } else g_string_append_printf(m, "\n"); } self = dbmail_message_init_with_string(self,m); dbmail_message_set_internal_date(self, internal_date); g_free(internal_date); g_string_free(m,TRUE); for (depth=0; blist[depth]; depth++) g_free(blist[depth]); g_free(blist); return self; } static gboolean store_mime_object(GMimeObject *object, DbmailMessage *m); static int store_head(GMimeObject *object, DbmailMessage *m) { int r; char *head = g_mime_object_get_headers(object); r = store_blob(m, head, 1); g_free(head); return r; } static int store_body(GMimeObject *object, DbmailMessage *m) { int r; char *text = g_mime_object_get_body(object); if (! text) return 0; r = store_blob(m, text, 0); g_free(text); return r; } static gboolean store_mime_text(GMimeObject *object, DbmailMessage *m, gboolean skiphead) { g_return_val_if_fail(GMIME_IS_OBJECT(object), TRUE); if (! skiphead && store_head(object, m) < 0) return TRUE; if(store_body(object, m) < 0) return TRUE; return FALSE; } static gboolean store_mime_multipart(GMimeObject *object, DbmailMessage *m, const GMimeContentType *content_type, gboolean skiphead) { const char *boundary; int n; g_return_val_if_fail(GMIME_IS_OBJECT(object), TRUE); boundary = g_mime_content_type_get_parameter(content_type,"boundary"); if (! skiphead && store_head(object,m) < 0) return TRUE; if (g_mime_content_type_is_type(content_type, "multipart", "*") && store_blob(m, g_mime_multipart_get_preface((GMimeMultipart *)object), 0) < 0) return TRUE; if (boundary) { m->part_depth++; n = m->part_order; m->part_order=0; } g_mime_multipart_foreach((GMimeMultipart *)object, (GMimePartFunc)store_mime_object, m); if (boundary) { n++; m->part_depth--; m->part_order=n; } if (g_mime_content_type_is_type(content_type, "multipart", "*") && store_blob(m, g_mime_multipart_get_postface((GMimeMultipart *)object), 0) < 0) return TRUE; return FALSE; } static gboolean store_mime_message(GMimeObject * object, DbmailMessage *m, gboolean skiphead) { gboolean r; GMimeMessage *m2; if (! skiphead && store_head(object, m) < 0) return TRUE; m2 = g_mime_message_part_get_message(GMIME_MESSAGE_PART(object)); if (GMIME_IS_MESSAGE(m2)) r = store_mime_object(GMIME_OBJECT(m2), m); else // fall-back r = store_mime_text(object, m, TRUE); g_object_unref(m2); return r; } gboolean store_mime_object(GMimeObject *object, DbmailMessage *m) { const GMimeContentType *content_type; GMimeObject *mime_part; gboolean r = FALSE; gboolean skiphead = FALSE; g_return_val_if_fail(GMIME_IS_OBJECT(object), TRUE); if (GMIME_IS_MESSAGE(object)) { dprint("\n<message>\n"); if(store_head(object,m) < 0) return TRUE; // we need to skip the first (post-rfc822) mime-headers // of the mime_part because they are already included as // part of the rfc822 headers skiphead = TRUE; g_mime_header_set_raw (GMIME_MESSAGE(object)->mime_part->headers, NULL); mime_part = g_mime_message_get_mime_part((GMimeMessage *)object); } else mime_part = object; content_type = g_mime_object_get_content_type(mime_part); if (g_mime_content_type_is_type(content_type, "multipart", "*")) { r = store_mime_multipart((GMimeObject *)mime_part, m, content_type, skiphead); } else if (g_mime_content_type_is_type(content_type, "message","*")) { r = store_mime_message((GMimeObject *)mime_part, m, skiphead); } else if (g_mime_content_type_is_type(content_type, "text","*")) { if (GMIME_IS_MESSAGE(object)) { if(store_body(object,m) < 0) r = TRUE; } else { r = store_mime_text((GMimeObject *)mime_part, m, skiphead); } } else { r = store_mime_text((GMimeObject *)mime_part, m, skiphead); } if (GMIME_IS_MESSAGE(object)) { g_object_unref(mime_part); dprint("\n</message>\n"); } return r; } gboolean dm_message_store(DbmailMessage *m) { return store_mime_object((GMimeObject *)m->content, m); } /* Useful for debugging. Uncomment if/when needed. *//* static void dump_to_file(const char *filename, const char *buf) { gint se; g_assert(filename); FILE *f = fopen(filename,"a"); if (! f) { se=errno; TRACE(TRACE_DEBUG,"opening dumpfile failed [%s]", strerror(se)); errno=se; return; } fprintf(f,"%s",buf); fclose(f); } */ /* \brief create a new empty DbmailMessage struct * \return the DbmailMessage */ DbmailMessage * dbmail_message_new(void) { DbmailMessage *self = g_new0(DbmailMessage,1); self->internal_date = time(NULL); self->envelope_recipient = g_string_new(""); /* provide quick case-insensitive header name searches */ self->header_name = g_tree_new((GCompareFunc)g_ascii_strcasecmp); /* provide quick case-sensitive header value searches */ self->header_value = g_tree_new((GCompareFunc)strcmp); /* internal cache: header_dict[headername.name] = headername.id */ self->header_dict = g_hash_table_new_full((GHashFunc)g_str_hash, (GEqualFunc)g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free); dbmail_message_set_class(self, DBMAIL_MESSAGE); return self; } void dbmail_message_free(DbmailMessage *self) { if (! self) return; if (self->headers) { g_relation_destroy(self->headers); self->headers = NULL; } if (self->content) { g_object_unref(self->content); self->content = NULL; } if (self->charset) { g_free(self->charset); self->charset = NULL; } g_string_free(self->envelope_recipient,TRUE); g_hash_table_destroy(self->header_dict); g_tree_destroy(self->header_name); g_tree_destroy(self->header_value); if (self->tmp) fclose(self->tmp); self->id=0; g_free(self); self = NULL; } /* \brief create and initialize a new DbmailMessage * \param FILE *instream from which to read * \param int streamtype is DBMAIL_STREAM_PIPE or DBMAIL_STREAM_LMTP * \return the new DbmailMessage */ DbmailMessage * dbmail_message_new_from_stream(FILE *instream, int streamtype) { GMimeStream *stream; DbmailMessage *message, *retmessage; assert(instream); message = dbmail_message_new(); stream = g_mime_stream_fs_new(dup(fileno(instream))); retmessage = dbmail_message_init_with_stream(message, stream, streamtype); g_object_unref(stream); if (retmessage) return retmessage; dbmail_message_free(message); return NULL; } /* \brief set the type flag for this DbmailMessage * \param the DbmailMessage on which to set the flag * \param type flag is either DBMAIL_MESSAGE or DBMAIL_MESSAGE_PART * \return non-zero in case of error */ int dbmail_message_set_class(DbmailMessage *self, int klass) { switch (klass) { case DBMAIL_MESSAGE: case DBMAIL_MESSAGE_PART: self->klass = klass; break; default: return 1; break; } return 0; } /* \brief accessor for the type flag * \return the flag */ int dbmail_message_get_class(const DbmailMessage *self) { return self->klass; } /* \brief initialize a previously created DbmailMessage using a GString * \param the empty DbmailMessage * \param GString *content contains the raw message * \return the filled DbmailMessage */ DbmailMessage * dbmail_message_init_with_string(DbmailMessage *self, const GString *content) { _set_content(self,content); if (! (GMIME_IS_MESSAGE(self->content))) { dbmail_message_set_class(self, DBMAIL_MESSAGE_PART); g_object_unref(self->content); self->content=NULL; _set_content(self, content); } _map_headers(self); return self; } DbmailMessage * dbmail_message_init_from_gmime_message(DbmailMessage *self, GMimeMessage *message) { g_return_val_if_fail(GMIME_IS_MESSAGE(message), NULL); self->content = GMIME_OBJECT(message); _map_headers(self); return self; } /* \brief initialize a previously created DbmailMessage using a GMimeStream * \param empty DbmailMessage * \param stream from which to read * \param type which indicates either pipe/network style streaming * \return the filled DbmailMessage */ DbmailMessage * dbmail_message_init_with_stream(DbmailMessage *self, GMimeStream *stream, dbmail_stream_t type) { int res; res = _set_content_from_stream(self,stream,type); if (res != 0) return NULL; _map_headers(self); return self; } static int _set_content(DbmailMessage *self, const GString *content) { int res; GMimeStream *stream; stream = g_mime_stream_mem_new_with_buffer(content->str, content->len+1); res = _set_content_from_stream(self, stream, DBMAIL_STREAM_PIPE); g_mime_stream_close(stream); g_object_unref(stream); return res; } static int _set_content_from_stream(DbmailMessage *self, GMimeStream *stream, dbmail_stream_t type) { #define MESSAGE_MAX_LINE_SIZE 1024 /* * We convert all messages to crlf->lf for internal usage and * db-insertion */ GMimeStream *fstream, *bstream, *mstream; GMimeFilter *filter; GMimeParser *parser; gchar *buf, *from = NULL; ssize_t getslen, putslen; int res = 0; gboolean firstline=TRUE; /* * buildup the memory stream buffer * we will read from stream until either EOF or <dot><crlf> is encountered * depending on the streamtype */ if (self->content) { g_object_unref(self->content); self->content=NULL; } parser = g_mime_parser_new(); switch(type) { case DBMAIL_STREAM_LMTP: case DBMAIL_STREAM_PIPE: buf = g_new0(char, MESSAGE_MAX_LINE_SIZE); // stream -> bstream (buffer) -> fstream (filter) -> mstream (in-memory copy) bstream = g_mime_stream_buffer_new(stream,GMIME_STREAM_BUFFER_BLOCK_READ); self->tmp = tmpfile(); if (! self->tmp) { int serr = errno; TRACE(TRACE_ERR, "opening tmpfile failed: %s", strerror(serr)); res = 1; break; } mstream = g_mime_stream_file_new(self->tmp); assert(mstream); fstream = g_mime_stream_filter_new_with_stream(mstream); g_mime_stream_file_set_owner((GMimeStreamFile *)mstream, FALSE); filter = g_mime_filter_crlf_new(GMIME_FILTER_CRLF_DECODE,GMIME_FILTER_CRLF_MODE_CRLF_DOTS); g_mime_stream_filter_add((GMimeStreamFilter *) fstream, filter); while ((getslen = g_mime_stream_buffer_gets(bstream, buf, MESSAGE_MAX_LINE_SIZE)) > 0) { if (firstline && strncmp(buf,"From ",5)==0) { from = g_strdup(buf); firstline=FALSE; continue; } if ((type==DBMAIL_STREAM_LMTP) && (strncmp(buf,".\r\n",3)==0)) break; putslen = g_mime_stream_write(fstream, buf, getslen); if (g_mime_stream_flush(fstream)) { TRACE(TRACE_ERR, "Failed to flush, is your /tmp filesystem full?"); res = 1; break; } if (putslen < getslen && getslen > putslen+1) { TRACE(TRACE_ERR, "Short write [%zd < %zd], is your /tmp filesystem full?", putslen, getslen); res = 1; break; } } if (getslen < 0) { TRACE(TRACE_ERR, "Read failed, did the client drop the connection?"); res = 1; } g_free(buf); g_mime_stream_reset(mstream); g_mime_parser_init_with_stream(parser, mstream); g_object_unref(fstream); g_object_unref(bstream); g_object_unref(mstream); g_object_unref(filter); break; default: case DBMAIL_STREAM_RAW: g_mime_parser_init_with_stream(parser, stream); break; } switch (dbmail_message_get_class(self)) { case DBMAIL_MESSAGE: TRACE(TRACE_DEBUG,"parse message"); self->content = GMIME_OBJECT(g_mime_parser_construct_message(parser)); // adding a header will prime the gmime message structure, but we want // to add an innocuous header // dbmail_message_set_header(self,"MIME-Version","1.0"); if (from) { dbmail_message_set_internal_date(self, from); g_free(from); } break; case DBMAIL_MESSAGE_PART: TRACE(TRACE_DEBUG,"parse part"); self->content = GMIME_OBJECT(g_mime_parser_construct_part(parser)); break; } g_object_unref(parser); return res; } static gboolean g_str_case_equal(gconstpointer a, gconstpointer b) { return MATCH((const char *)a,(const char *)b); } static void _map_headers(DbmailMessage *self) { GMimeObject *part; assert(self->content); if (self->headers) g_relation_destroy(self->headers); self->headers = g_relation_new(2); g_relation_index(self->headers, 0, (GHashFunc)g_str_hash, (GEqualFunc)g_str_case_equal); g_relation_index(self->headers, 1, (GHashFunc)g_str_hash, (GEqualFunc)g_str_case_equal); if (GMIME_IS_MESSAGE(self->content)) { char *message_id = NULL; char *type = NULL; // this is needed to correctly initialize gmime's mime iterator // if (GMIME_MESSAGE(self->content)->mime_part) // g_mime_header_set_raw (GMIME_MESSAGE(self->content)->mime_part->headers, NULL); /* make sure the message has a message-id, else threading breaks */ if (! (message_id = (char *)g_mime_message_get_message_id(GMIME_MESSAGE(self->content)))) { char *domainname = g_new0(gchar, 255); if (getdomainname(domainname,255)) strcpy(domainname,"(none)"); message_id = g_mime_utils_generate_message_id(domainname); g_mime_message_set_message_id(GMIME_MESSAGE(self->content), message_id); g_free(message_id); g_free(domainname); } // gmime doesn't consider the content-type header to be a message-header so extract // and register it separately part = g_mime_message_get_mime_part(GMIME_MESSAGE(self->content)); if ((type = (char *)g_mime_object_get_header(part,"Content-Type"))!=NULL) _register_header("Content-Type",type, (gpointer)self); g_object_unref(part); } g_mime_header_foreach(GMIME_OBJECT(self->content)->headers, _register_header, self); } static void _register_header(const char *header, const char *value, gpointer user_data) { const char *hname, *hvalue; DbmailMessage *m = (DbmailMessage *)user_data; assert(header); assert(value); assert(m); if (! (hname = g_tree_lookup(m->header_name,header))) { g_tree_insert(m->header_name,(gpointer)header,(gpointer)header); hname = header; } if (! (hvalue = g_tree_lookup(m->header_value,value))) { g_tree_insert(m->header_value,(gpointer)value,(gpointer)value); hvalue = value; } if (m->headers && (! g_relation_exists(m->headers, hname, hvalue))) g_relation_insert(m->headers, hname, hvalue); } void dbmail_message_set_physid(DbmailMessage *self, u64_t physid) { self->id = physid; self->physid = physid; } u64_t dbmail_message_get_physid(const DbmailMessage *self) { return self->physid; } void dbmail_message_set_internal_date(DbmailMessage *self, char *internal_date) { if (internal_date && strlen(internal_date)) self->internal_date = g_mime_utils_header_decode_date(internal_date, self->internal_date_gmtoff); else self->internal_date = time(NULL); } /* thisyear is a workaround for some broken gmime version. */ gchar * dbmail_message_get_internal_date(const DbmailMessage *self, int thisyear) { char *res; struct tm gmt; assert(self->internal_date); res = g_new0(char, TIMESTRING_SIZE+1); memset(&gmt,'\0', sizeof(struct tm)); gmtime_r(&self->internal_date, &gmt); /* override if the date is not sane */ if (thisyear && gmt.tm_year + 1900 > thisyear + 1) { gmt.tm_year = thisyear - 1900; } strftime(res, TIMESTRING_SIZE, "%Y-%m-%d %T", &gmt); return res; } void dbmail_message_set_envelope_recipient(DbmailMessage *self, const char *envelope_recipient) { if (envelope_recipient) g_string_printf(self->envelope_recipient,"%s", envelope_recipient); } gchar * dbmail_message_get_envelope_recipient(const DbmailMessage *self) { if (self->envelope_recipient->len > 0) return self->envelope_recipient->str; return NULL; } void dbmail_message_set_header(DbmailMessage *self, const char *header, const char *value) { g_mime_message_set_header(GMIME_MESSAGE(self->content), header, value); if (self->headers) _map_headers(self); } const gchar * dbmail_message_get_header(const DbmailMessage *self, const char *header) { return g_mime_message_get_header(GMIME_MESSAGE(self->content), header); } GTuples * dbmail_message_get_header_repeated(const DbmailMessage *self, const char *header) { const char *hname; if (! (hname = g_tree_lookup(self->header_name,header))) hname = header; return g_relation_select(self->headers, hname, 0); } GList * dbmail_message_get_header_addresses(DbmailMessage *message, const char *field_name) { InternetAddressList *ialisthead, *ialist; InternetAddress *ia; GList *result = NULL; const char *field_value; if (!message || !field_name) { TRACE(TRACE_WARNING, "received a NULL argument, this is a bug"); return NULL; } field_value = dbmail_message_get_header(message, field_name); TRACE(TRACE_INFO, "mail address parser looking at field [%s] with value [%s]", field_name, field_value); if ((ialist = internet_address_parse_string(field_value)) == NULL) { TRACE(TRACE_NOTICE, "mail address parser error parsing header field"); return NULL; } ialisthead = ialist; while (1) { ia = ialist->address; result = g_list_append(result, g_strdup(ia->value.addr)); if (! ialist->next) break; ialist = ialist->next; } internet_address_list_destroy(ialisthead); TRACE(TRACE_DEBUG, "mail address parser found [%d] email addresses", g_list_length(result)); return result; } char * dbmail_message_get_charset(DbmailMessage *self) { assert(self && self->content); if (! self->charset) self->charset = message_get_charset((GMimeMessage *)self->content); return self->charset; } /* dump message(parts) to char ptrs */ gchar * dbmail_message_to_string(const DbmailMessage *self) { assert(self && self->content); return g_mime_object_to_string(GMIME_OBJECT(self->content)); } gchar * dbmail_message_body_to_string(const DbmailMessage *self) { assert(self && self->content); return g_mime_object_get_body(GMIME_OBJECT(self->content)); } gchar * dbmail_message_hdrs_to_string(const DbmailMessage *self) { gchar *h; unsigned i = 0; h = dbmail_message_to_string(self); i = find_end_of_header(h); h[i] = '\0'; h = g_realloc(h, i+1); return h; } /* * Some dynamic accessors. * * Don't cache these values to allow changes in message content!! * */ size_t dbmail_message_get_size(const DbmailMessage *self, gboolean crlf) { char *s, *t; size_t r; s = dbmail_message_to_string(self); if (crlf) { t = get_crlf_encoded(s); r = strlen(t); g_free(t); } else { r = strlen(s); } g_free(s); return r; } size_t dbmail_message_get_hdrs_size(const DbmailMessage *self, gboolean crlf) { char *s, *t; size_t r; s = dbmail_message_hdrs_to_string(self); if (crlf) { t = get_crlf_encoded(s); r = strlen(t); g_free(t); } else { r = strlen(s); } g_free(s); return r; } size_t dbmail_message_get_body_size(const DbmailMessage *self, gboolean crlf) { char *s, *t; size_t r; s = dbmail_message_body_to_string(self); if (! s) return 0; if (crlf) { t = get_crlf_encoded(s); r = strlen(t); g_free(t); } else { r = strlen(s); } g_free(s); return r; } static DbmailMessage * _retrieve(DbmailMessage *self, const char *query_template) { int l, row = 0; GString *m; INIT_QUERY; C c; R r; DbmailMessage *store; field_t frag; char *internal_date = NULL; gconstpointer blob; assert(dbmail_message_get_physid(self)); store = self; if ((self = _mime_retrieve(self))) return self; self = store; date2char_str("p.internal_date", &frag); snprintf(query, DEF_QUERYSIZE, query_template, frag, DBPFX, DBPFX, dbmail_message_get_physid(self)); c = db_con_get(); if (! (r = db_query(c, query))) { db_con_close(c); return NULL; } row = 0; m = g_string_new(""); while (db_result_next(r)) { blob = db_result_get_blob(r,0,&l); char *str = g_new0(char,l+1); str = strncpy(str, blob, l); if (row == 0) internal_date = g_strdup(db_result_get(r,2)); g_string_append_printf(m, "%s", str); g_free(str); row++; } db_con_close(c); self = dbmail_message_init_with_string(self,m); if (internal_date && strlen(internal_date)) dbmail_message_set_internal_date(self, internal_date); if (internal_date) g_free(internal_date); g_string_free(m,TRUE); return self; } /* * * retrieve the header messageblk * * TODO: this call is yet unused in the code, but here for * forward compatibility's sake. * */ static DbmailMessage * _fetch_head(DbmailMessage *self) { const char *query_template = "SELECT b.messageblk, b.is_header, %s " "FROM %smessageblks b " "JOIN %sphysmessage p ON b.physmessage_id=p.id " "WHERE b.physmessage_id = %llu " "AND b.is_header = '1'"; return _retrieve(self, query_template); } /* * * retrieve the full message * */ static DbmailMessage * _fetch_full(DbmailMessage *self) { const char *query_template = "SELECT b.messageblk, b.is_header, %s " "FROM %smessageblks b " "JOIN %sphysmessage p ON b.physmessage_id=p.id " "WHERE b.physmessage_id = %llu " "ORDER BY b.messageblk_idnr"; return _retrieve(self, query_template); } /* \brief retrieve message * \param empty DbmailMessage * \param physmessage_id * \param filter (header-only or full message) * \return filled DbmailMessage */ DbmailMessage * dbmail_message_retrieve(DbmailMessage *self, u64_t physid, int filter) { assert(physid); dbmail_message_set_physid(self, physid); switch (filter) { case DBMAIL_MESSAGE_FILTER_HEAD: self = _fetch_head(self); break; case DBMAIL_MESSAGE_FILTER_BODY: case DBMAIL_MESSAGE_FILTER_FULL: self = _fetch_full(self); break; } if ((!self) || (! self->content)) { TRACE(TRACE_ERR, "retrieval failed for physid [%llu]", physid); return NULL; } return self; } /* \brief store a temporary copy of a message. * \param filled DbmailMessage * \return * - -1 on error * - 1 on success */ int dbmail_message_store(DbmailMessage *self) { u64_t user_idnr; char unique_id[UID_SIZE]; int res = 0; u64_t hdrs_size, body_size, rfcsize; int i=1, retry=10, delay=200; if (! auth_user_exists(DBMAIL_DELIVERY_USERNAME, &user_idnr)) { TRACE(TRACE_ERR, "unable to find user_idnr for user [%s]. Make sure this system user is in the database!", DBMAIL_DELIVERY_USERNAME); return DM_EQUERY; } create_unique_id(unique_id, user_idnr); while (i++ < retry) { /* create a message record */ if(_message_insert(self, user_idnr, DBMAIL_TEMPMBOX, unique_id) < 0) { usleep(delay*i); continue; } hdrs_size = (u64_t)dbmail_message_get_hdrs_size(self, FALSE); body_size = (u64_t)dbmail_message_get_body_size(self, FALSE); if ((res = dm_message_store(self))) { TRACE(TRACE_WARNING,"Failed to store mimeparts"); usleep(delay*i); continue; } rfcsize = (u64_t)dbmail_message_get_size(self,TRUE); if (( res = db_update_message(self->id, unique_id, (hdrs_size + body_size), rfcsize)) < 0) { usleep(delay*i); continue; } /* store message headers */ if ((res = dbmail_message_cache_headers(self)) < 0) { usleep(delay*i); continue; } /* ready */ break; } return res; } static void insert_physmessage(DbmailMessage *self, C c) { R r; char *internal_date, *frag; int thisyear; volatile u64_t id = 0; struct timeval tv; struct tm gmt; /* get the messages date, but override it if it's from the future */ gettimeofday(&tv, NULL); localtime_r(&tv.tv_sec, &gmt); thisyear = gmt.tm_year + 1900; internal_date = dbmail_message_get_internal_date(self, thisyear); frag = db_returning("id"); if (internal_date != NULL) { field_t to_date_str; char2date_str(internal_date, &to_date_str); r = db_query(c, "INSERT INTO %sphysmessage (internal_date) VALUES (%s) %s", DBPFX, &to_date_str, frag); g_free(frag); } else { r = db_query(c, "INSERT INTO %sphysmessage (internal_date) VALUES (%s) %s", DBPFX, db_get_sql(SQL_CURRENT_TIMESTAMP), frag); g_free(frag); } id = db_insert_result(c, r); if (! id) { TRACE(TRACE_ERR,"no physmessage_id [%llu]", id); } else { dbmail_message_set_physid(self, id); TRACE(TRACE_DEBUG,"new physmessage_id [%llu]", id); } } int _message_insert(DbmailMessage *self, u64_t user_idnr, const char *mailbox, const char *unique_id) { u64_t mailboxid; char *frag = NULL; C c; R r; volatile int t = 0; assert(unique_id); assert(mailbox); if (db_find_create_mailbox(mailbox, BOX_DEFAULT, user_idnr, &mailboxid) == -1) return -1; if (mailboxid == 0) { TRACE(TRACE_ERR, "mailbox [%s] could not be found!", mailbox); return -1; } /* insert a new physmessage entry */ /* now insert an entry into the messages table */ c = db_con_get(); TRY db_begin_transaction(c); insert_physmessage(self, c); frag = db_returning("message_idnr"); r = db_query(c, "INSERT INTO " "%smessages(mailbox_idnr, physmessage_id, unique_id," "recent_flag, status) " "VALUES (%llu, %llu, '%s', 1, %d) %s", DBPFX, mailboxid, dbmail_message_get_physid(self), unique_id, MESSAGE_STATUS_INSERT, frag); g_free(frag); self->id = db_insert_result(c, r); TRACE(TRACE_DEBUG,"new message_idnr [%llu]", self->id); t = DM_SUCCESS; db_commit_transaction(c); CATCH(SQLException) LOG_SQLERROR; t = DM_EQUERY; FINALLY db_con_close(c); END_TRY; return t; } int dbmail_message_cache_headers(const DbmailMessage *self) { assert(self); assert(self->physid); if (! GMIME_IS_MESSAGE(self->content)) { TRACE(TRACE_ERR,"self->content is not a message"); return -1; } g_tree_foreach(self->header_name, (GTraverseFunc)_header_cache, (gpointer)self); dbmail_message_cache_referencesfield(self); dbmail_message_cache_envelope(self); return DM_SUCCESS; } #define CACHE_WIDTH 255 static int _header_name_get_id(const DbmailMessage *self, const char *header, u64_t *id) { u64_t *tmp = NULL; gpointer cacheid; gchar *case_header, *safe_header, *frag; C c; R r; S s; volatile int t = FALSE; // rfc822 headernames are case-insensitive safe_header = g_ascii_strdown(header,-1); if ((cacheid = g_hash_table_lookup(self->header_dict, (gconstpointer)safe_header)) != NULL) { *id = *(u64_t *)cacheid; g_free(safe_header); return 1; } case_header = g_strdup_printf(db_get_sql(SQL_STRCASE),"headername"); tmp = g_new0(u64_t,1); c = db_con_get(); TRY *tmp = 0; s = db_stmt_prepare(c, "SELECT id FROM %sheadername WHERE %s=?", DBPFX, case_header); db_stmt_set_str(s,1,safe_header); r = db_stmt_query(s); if (db_result_next(r)) { *tmp = db_result_get_u64(r,0); } else { db_con_clear(c); frag = db_returning("id"); s = db_stmt_prepare(c, "INSERT %s INTO %sheadername (headername) VALUES (?) %s", db_get_sql(SQL_IGNORE), DBPFX, frag); g_free(frag); db_stmt_set_str(s,1,safe_header); r = db_stmt_query(s); *tmp = db_insert_result(c, r); } t = TRUE; CATCH(SQLException) LOG_SQLERROR; t = DM_EQUERY; FINALLY db_con_close(c); END_TRY; g_free(case_header); if (t == DM_EQUERY) { g_free(safe_header); g_free(tmp); return t; } *id = *tmp; g_hash_table_insert(self->header_dict, (gpointer)(safe_header), (gpointer)(tmp)); return 1; } static u64_t _header_value_exists(C c, const char *value, const char *hash) { R r; S s; u64_t id = 0; db_con_clear(c); s = db_stmt_prepare(c, "SELECT id FROM %sheadervalue WHERE hash=? and headervalue=?", DBPFX); db_stmt_set_str(s,1 , hash); db_stmt_set_blob(s, 2, value, strlen(value)); r = db_stmt_query(s); if (db_result_next(r)) id = db_result_get_u64(r,0); return id; } static u64_t _header_value_insert(C c, const char *value, const char *sortfield, const char *datefield, const char *hash) { R r; S s; u64_t id = 0; char *frag; db_con_clear(c); frag = db_returning("id"); s = db_stmt_prepare(c, "INSERT INTO %sheadervalue (hash, headervalue, sortfield, datefield) VALUES (?,?,?,?) %s", DBPFX, frag); g_free(frag); db_stmt_set_str(s, 1, hash); db_stmt_set_blob(s, 2, value, strlen(value)); db_stmt_set_str(s, 3, sortfield); db_stmt_set_str(s, 4, datefield); r = db_stmt_query(s); id = db_insert_result(c, r); TRACE(TRACE_DATABASE,"new headervalue.id [%llu]", id); return id; } static int _header_value_get_id(const char *value, const char *sortfield, const char *datefield, u64_t *id) { u64_t tmp = 0; char *hash; C c; hash = dm_get_hash_for_string(value); if (! hash) return FALSE; c = db_con_get(); TRY if ((tmp = _header_value_exists(c, value, (const char *)hash)) != 0) *id = tmp; else if ((tmp = _header_value_insert(c, value, sortfield, datefield, (const char *)hash)) != 0) *id = tmp; CATCH(SQLException) LOG_SQLERROR; FINALLY db_con_close(c); END_TRY; assert(*id); g_free(hash); return TRUE; } static gboolean _header_insert(u64_t physmessage_id, u64_t headername_id, u64_t headervalue_id) { C c; S s; volatile gboolean t = TRUE; c = db_con_get(); TRY s = db_stmt_prepare(c, "INSERT INTO %sheader (physmessage_id, headername_id, headervalue_id) VALUES (?,?,?)", DBPFX); db_stmt_set_u64(s, 1, physmessage_id); db_stmt_set_u64(s, 2, headername_id); db_stmt_set_u64(s, 3, headervalue_id); db_stmt_exec(s); CATCH(SQLException) LOG_SQLERROR; t = FALSE; FINALLY db_con_close(c); END_TRY; return t; } static gboolean _header_cache(const char UNUSED *key, const char *header, gpointer user_data) { u64_t headername_id; u64_t headervalue_id; DbmailMessage *self = (DbmailMessage *)user_data; GTuples *values; unsigned char *raw; unsigned i; time_t date; volatile gboolean isaddr = 0, isdate = 0, issubject = 0; const char *charset = dbmail_message_get_charset(self); gchar *sortfield = NULL, *datefield = NULL; InternetAddressList *emaillist; InternetAddress *ia; /* skip headernames with spaces like From_ */ if (strchr(header, ' ')) return FALSE; TRACE(TRACE_DEBUG,"headername [%s]", header); if ((_header_name_get_id(self, header, &headername_id) < 0)) return TRUE; if (g_ascii_strcasecmp(header,"From")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"To")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"Reply-to")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"Cc")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"Bcc")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"Return-path")==0) isaddr=1; else if (g_ascii_strcasecmp(header,"Subject")==0) issubject=1; else if (g_ascii_strcasecmp(header,"Date")==0) isdate=1; values = g_relation_select(self->headers,header,0); for (i=0; i<values->len;i++) { char *value = NULL; raw = (unsigned char *)g_tuples_index(values,i,1); TRACE(TRACE_DEBUG,"raw header value [%s]", raw); value = dbmail_iconv_decode_field((const char *)raw, charset, isaddr); if ((! value) || (strlen(value) == 0)) { if (value) g_free(value); continue; } // Generate additional fields for SORT optimization if(isaddr) { emaillist = internet_address_parse_string(value); for (; emaillist != NULL && emaillist->address; emaillist = emaillist->next) { ia = emaillist->address; if(ia == NULL) break; if(sortfield == NULL) { // Only the first email recipient is to be used for sorting - so save it now. sortfield = g_strndup(ia->value.addr ? ia->value.addr : "", CACHE_WIDTH); } } internet_address_list_destroy(emaillist); } if(issubject) { sortfield = dbmail_iconv_str_to_db(dm_base_subject(value), charset); } if(isdate) { date = g_mime_utils_header_decode_date(value,NULL); if (date == (time_t)-1) date = (time_t)0; datefield = g_new0(gchar,20); strftime(datefield,20,"%Y-%m-%d %H:%M:%S",gmtime(&date)); TRACE(TRACE_DEBUG,"Date is [%ld], datefield [%s]",date,datefield); } if (! sortfield) sortfield = g_strndup(value, CACHE_WIDTH); /* avoid column overflow */ if (strlen(value) > CACHE_WIDTH) value[CACHE_WIDTH-1] = '\0'; if (strlen(sortfield) > CACHE_WIDTH) sortfield[CACHE_WIDTH-1] = '\0'; /* Fetch header value id if exists, else insert, and return new id */ _header_value_get_id(value, sortfield, datefield, &headervalue_id); g_free(value); /* Insert relation between physmessage, header name and header value */ _header_insert(self->physid, headername_id, headervalue_id); headervalue_id=0; g_free(sortfield); sortfield = NULL; g_free(datefield); datefield = NULL; emaillist=NULL; date=0; } g_tuples_destroy(values); return FALSE; } static void insert_field_cache(u64_t physid, const char *field, const char *value) { gchar *clean_value; C c; S s; g_return_if_fail(value != NULL); /* field values are truncated to 255 bytes */ clean_value = g_strndup(value,CACHE_WIDTH); c = db_con_get(); TRY s = db_stmt_prepare(c,"INSERT INTO %s%sfield (physmessage_id, %sfield) VALUES (?,?)", DBPFX, field, field); db_stmt_set_u64(s, 1, physid); db_stmt_set_str(s, 2, clean_value); db_stmt_exec(s); CATCH(SQLException) LOG_SQLERROR; TRACE(TRACE_ERR, "insert %sfield failed [%s]", field, value); FINALLY db_con_close(c); END_TRY; g_free(clean_value); } #define DM_ADDRESS_TYPE_TO "To" #define DM_ADDRESS_TYPE_CC "Cc" #define DM_ADDRESS_TYPE_FROM "From" #define DM_ADDRESS_TYPE_REPL "Reply-to" void dbmail_message_cache_referencesfield(const DbmailMessage *self) { GMimeReferences *refs, *head; GTree *tree; const char *referencesfield, *inreplytofield, *field; referencesfield = (char *)dbmail_message_get_header(self,"References"); inreplytofield = (char *)dbmail_message_get_header(self,"In-Reply-To"); // Some clients will put parent in the in-reply-to header only and the grandparents and older in references field = g_strconcat(referencesfield, " ", inreplytofield, NULL); refs = g_mime_references_decode(field); if (! refs) { TRACE(TRACE_DEBUG, "reference_decode failed [%llu]", self->physid); return; } head = refs; tree = g_tree_new_full((GCompareDataFunc)dm_strcmpdata, NULL, NULL, NULL); while (refs->msgid) { if (! g_tree_lookup(tree,refs->msgid)) { insert_field_cache(self->physid, "references", refs->msgid); g_tree_insert(tree,refs->msgid,refs->msgid); } if (refs->next == NULL) break; refs = refs->next; } g_tree_destroy(tree); g_mime_references_clear(&head); } void dbmail_message_cache_envelope(const DbmailMessage *self) { char *envelope = NULL; C c; S s; envelope = imap_get_envelope(GMIME_MESSAGE(self->content)); c = db_con_get(); TRY s = db_stmt_prepare(c, "INSERT INTO %senvelope (physmessage_id, envelope) VALUES (?,?)", DBPFX); db_stmt_set_u64(s, 1, self->physid); db_stmt_set_str(s, 2, envelope); db_stmt_exec(s); CATCH(SQLException) LOG_SQLERROR; TRACE(TRACE_ERR, "insert envelope failed [%s]", envelope); FINALLY db_con_close(c); END_TRY; g_free(envelope); envelope = NULL; } // // construct a new message where only sender, recipient, subject and // a body are known. The body can be any kind of charset. Make sure // it's not pre-encoded (base64, quopri) // // TODO: support text/html DbmailMessage * dbmail_message_construct(DbmailMessage *self, const gchar *to, const gchar *from, const gchar *subject, const gchar *body) { GMimeMessage *message; GMimePart *mime_part; GMimeDataWrapper *content; GMimeStream *stream, *fstream; GMimeContentType *mime_type; GMimePartEncodingType encoding = GMIME_PART_ENCODING_DEFAULT; GMimeFilter *filter = NULL; // FIXME: this could easily be expanded to allow appending // a new sub-part to an existing mime-part. But for now let's // require self to be a pristine (empty) DbmailMessage. g_return_val_if_fail(self->content==NULL, self); message = g_mime_message_new(FALSE); // determine the optimal encoding type for the body: how would gmime // encode this string. This will return either base64 or quopri. if (g_mime_utils_text_is_8bit((unsigned char *)body, strlen(body))) encoding = g_mime_utils_best_encoding((unsigned char *)body, strlen(body)); // set basic headers g_mime_message_set_sender(message, from); g_mime_message_set_subject(message, subject); g_mime_message_add_recipients_from_string(message, GMIME_RECIPIENT_TYPE_TO, to); // construct mime-part mime_part = g_mime_part_new(); // setup a stream-filter stream = g_mime_stream_mem_new(); fstream = g_mime_stream_filter_new_with_stream(stream); switch(encoding) { case GMIME_PART_ENCODING_BASE64: filter = g_mime_filter_basic_new_type(GMIME_FILTER_BASIC_BASE64_ENC); break; case GMIME_PART_ENCODING_QUOTEDPRINTABLE: filter = g_mime_filter_basic_new_type(GMIME_FILTER_BASIC_QP_ENC); break; default: break; } if (filter) { g_mime_stream_filter_add((GMimeStreamFilter *)fstream, filter); g_object_unref(filter); } // fill the stream and thus the mime-part g_mime_stream_write_string(fstream,body); content = g_mime_data_wrapper_new_with_stream(stream, encoding); g_mime_part_set_content_object(mime_part, content); // add the correct mime-headers // Content-Type mime_type = g_mime_content_type_new("text","plain"); g_mime_object_set_content_type((GMimeObject *)mime_part, mime_type); // We originally tried to use g_mime_charset_best to pick a charset, // but it regularly failed to choose utf-8 when utf-8 data was given to it. g_mime_object_set_content_type_parameter((GMimeObject *)mime_part, "charset", "utf-8"); // Content-Transfer-Encoding switch(encoding) { case GMIME_PART_ENCODING_BASE64: g_mime_part_set_content_header(mime_part,"Content-Transfer-Encoding", "base64"); break; case GMIME_PART_ENCODING_QUOTEDPRINTABLE: g_mime_part_set_content_header(mime_part,"Content-Transfer-Encoding", "quoted-printable"); break; default: g_mime_part_set_content_header(mime_part,"Content-Transfer-Encoding", "7bit"); break; } // attach the mime-part to the mime-message g_mime_message_set_mime_part(message, (GMimeObject *)mime_part); // attach the message to the DbmailMessage struct self->content = (GMimeObject *)message; // cleanup g_object_unref(mime_part); g_object_unref(content); g_object_unref(stream); g_object_unref(fstream); return self; } static int get_mailbox_from_filters(DbmailMessage *message, u64_t useridnr, const char *mailbox, char *into, size_t into_n) { int t = FALSE; u64_t anyone = 0; C c; R r; TRACE(TRACE_INFO, "default mailbox [%s]", mailbox); if (mailbox != NULL) return t; if (! auth_user_exists(DBMAIL_ACL_ANYONE_USER, &anyone)) return t; c = db_con_get(); TRY r = db_query(c, "SELECT f.mailbox,f.headername,f.headervalue FROM %sfilters f " "JOIN %sheadername n ON f.headername=n.headername " "JOIN %sheader h ON h.headername_id = n.id " "join %sheadervalue v on v.id=h.headervalue_id " "WHERE v.headervalue %s f.headervalue " "AND h.physmessage_id=%llu " "AND f.user_id in (%llu,%llu)", DBPFX, DBPFX, DBPFX, DBPFX, db_get_sql(SQL_INSENSITIVE_LIKE), dbmail_message_get_physid(message), anyone, useridnr); if (db_result_next(r)) { const char *hn, *hv; strncpy(into, db_result_get(r,0), into_n); hn = db_result_get(r,1); hv = db_result_get(r,2); TRACE(TRACE_DEBUG, "match [%s: %s] file-into mailbox [%s]", hn, hv, into); t = TRUE; } CATCH(SQLException) LOG_SQLERROR; FINALLY db_con_close(c); END_TRY; return t; } /* Figure out where to deliver the message, then deliver it. * */ dsn_class_t sort_and_deliver(DbmailMessage *message, const char *destination, u64_t useridnr, const char *mailbox, mailbox_source_t source) { int cancelkeep = 0; int reject = 0; dsn_class_t ret; field_t val; char *subaddress = NULL; /* Catch the brute force delivery right away. * We skip the Sieve scripts, and down the call * chain we don't check permissions on the mailbox. */ if (source == BOX_BRUTEFORCE) { TRACE(TRACE_NOTICE, "Beginning brute force delivery for user [%llu] to mailbox [%s].", useridnr, mailbox); return sort_deliver_to_mailbox(message, useridnr, mailbox, source, NULL); } /* This is the only condition when called from pipe.c, actually. */ if (! mailbox) { char into[1024]; memset(into,0,sizeof(into)); if (! (get_mailbox_from_filters(message, useridnr, mailbox, into, sizeof(into)))) { mailbox = "INBOX"; source = BOX_DEFAULT; } else { mailbox = into; } } TRACE(TRACE_INFO, "Destination [%s] useridnr [%llu], mailbox [%s], source [%d]", destination, useridnr, mailbox, source); /* Subaddress. */ config_get_value("SUBADDRESS", "DELIVERY", val); if (strcasecmp(val, "yes") == 0) { int res; size_t sublen, subpos; res = find_bounded((char *)destination, '+', '@', &subaddress, &sublen, &subpos); if (res == 0 && sublen > 0) { /* We'll free this towards the end of the function. */ mailbox = subaddress; source = BOX_ADDRESSPART; TRACE(TRACE_INFO, "Setting BOX_ADDRESSPART mailbox to [%s]", mailbox); } } /* Give Sieve access to the envelope recipient. */ dbmail_message_set_envelope_recipient(message, destination); /* Sieve. */ config_get_value("SIEVE", "DELIVERY", val); if (strcasecmp(val, "yes") == 0 && dm_sievescript_isactive(useridnr)) { TRACE(TRACE_INFO, "Calling for a Sieve sort"); sort_result_t *sort_result = sort_process(useridnr, message); if (sort_result) { cancelkeep = sort_get_cancelkeep(sort_result); reject = sort_get_reject(sort_result); sort_free_result(sort_result); } } /* Sieve actions: * (m = must implement, s = should implement, e = extension) * m Keep - implicit default action. * m Discard - requires us to skip the default action. * m Redirect - add to the forwarding list. * s Fileinto - change the destination mailbox. * s Reject - nope, sorry. we killed bounce(). * e Vacation - share with the auto reply code. */ if (cancelkeep) { // The implicit keep has been cancelled. // This may necessarily imply that the message // is being discarded -- dropped flat on the floor. ret = DSN_CLASS_OK; TRACE(TRACE_INFO, "Keep was cancelled. Message may be discarded."); } else { ret = sort_deliver_to_mailbox(message, useridnr, mailbox, source, NULL); TRACE(TRACE_INFO, "Keep was not cancelled. Message will be delivered by default."); } /* Might have been allocated by the subaddress calculation. NULL otherwise. */ g_free(subaddress); /* Reject probably implies cancelkeep, * but we'll not assume that and instead * just test this as a separate block. */ if (reject) { TRACE(TRACE_INFO, "Message will be rejected."); ret = DSN_CLASS_FAIL; } return ret; } dsn_class_t sort_deliver_to_mailbox(DbmailMessage *message, u64_t useridnr, const char *mailbox, mailbox_source_t source, int *msgflags) { u64_t mboxidnr, newmsgidnr; field_t val; size_t msgsize = (u64_t)dbmail_message_get_size(message, FALSE); TRACE(TRACE_INFO,"useridnr [%llu] mailbox [%s]", useridnr, mailbox); if (db_find_create_mailbox(mailbox, source, useridnr, &mboxidnr) != 0) { TRACE(TRACE_ERR, "mailbox [%s] not found", mailbox); return DSN_CLASS_FAIL; } if (source == BOX_BRUTEFORCE) { TRACE(TRACE_INFO, "Brute force delivery; skipping ACL checks on mailbox."); } else { // Check ACL's on the mailbox. It must be read-write, // it must not be no_select, and it may require an ACL for // the user whose Sieve script this is, since it's possible that // we've looked up a #Public or a #Users mailbox. int permission; TRACE(TRACE_DEBUG, "Checking if we have the right to post incoming messages"); MailboxState_T S = MailboxState_new(mboxidnr); MailboxState_reload(S, useridnr); permission = acl_has_right(S, useridnr, ACL_RIGHT_POST); MailboxState_free(&S); switch (permission) { case -1: TRACE(TRACE_NOTICE, "error retrieving right for [%llu] to deliver mail to [%s]", useridnr, mailbox); return DSN_CLASS_TEMP; case 0: // No right. TRACE(TRACE_NOTICE, "user [%llu] does not have right to deliver mail to [%s]", useridnr, mailbox); // Switch to INBOX. if (strcmp(mailbox, "INBOX") == 0) { // Except if we've already been down this path. TRACE(TRACE_NOTICE, "already tried to deliver to INBOX"); return DSN_CLASS_FAIL; } return sort_deliver_to_mailbox(message, useridnr, "INBOX", BOX_DEFAULT, msgflags); case 1: // Has right. TRACE(TRACE_INFO, "user [%llu] has right to deliver mail to [%s]", useridnr, mailbox); break; default: TRACE(TRACE_ERR, "invalid return value from acl_has_right"); return DSN_CLASS_FAIL; } } // if the mailbox already holds this message we're done GETCONFIGVALUE("suppress_duplicates", "DELIVERY", val); if (strcasecmp(val,"yes")==0) { const char *messageid = dbmail_message_get_header(message, "message-id"); if ( messageid && ((db_mailbox_has_message_id(mboxidnr, messageid)) > 0) ) { TRACE(TRACE_INFO, "suppress_duplicate: [%s]", messageid); return DSN_CLASS_OK; } } // Ok, we have the ACL right, time to deliver the message. switch (db_copymsg(message->id, mboxidnr, useridnr, &newmsgidnr)) { case -2: TRACE(TRACE_ERR, "error copying message to user [%llu]," "maxmail exceeded", useridnr); return DSN_CLASS_QUOTA; case -1: TRACE(TRACE_ERR, "error copying message to user [%llu]", useridnr); return DSN_CLASS_TEMP; default: TRACE(TRACE_NOTICE, "message id=%llu, size=%zd is inserted", newmsgidnr, msgsize); if (msgflags) { TRACE(TRACE_NOTICE, "message id=%llu, setting imap flags", newmsgidnr); db_set_msgflag(newmsgidnr, msgflags, NULL, IMAPFA_ADD, NULL); db_mailbox_seq_update(mboxidnr); } message->id = newmsgidnr; return DSN_CLASS_OK; } } static int parse_and_escape(const char *in, char **out) { InternetAddressList *ialist; InternetAddress *ia; TRACE(TRACE_DEBUG, "parsing address [%s]", in); ialist = internet_address_parse_string(in); if (!ialist) { TRACE(TRACE_NOTICE, "unable to parse email address [%s]", in); return -1; } ia = ialist->address; if (!ia || ia->type != INTERNET_ADDRESS_NAME) { TRACE(TRACE_NOTICE, "unable to parse email address [%s]", in); internet_address_list_destroy(ialist); return -1; } if (! (*out = dm_shellesc(ia->value.addr))) { TRACE(TRACE_ERR, "out of memory calling dm_shellesc"); internet_address_list_destroy(ialist); return -1; } internet_address_list_destroy(ialist); return 0; } /* Sends a message. */ int send_mail(DbmailMessage *message, const char *to, const char *from, const char *preoutput, enum sendwhat sendwhat, char *sendmail_external) { FILE *mailpipe = NULL; char *escaped_to = NULL; char *escaped_from = NULL; char *message_string = NULL; char *sendmail_command = NULL; field_t sendmail, postmaster; int result; if (!from || strlen(from) < 1) { if (config_get_value("POSTMASTER", "DBMAIL", postmaster) < 0) { TRACE(TRACE_NOTICE, "no config value for POSTMASTER"); } if (strlen(postmaster)) from = postmaster; else from = DEFAULT_POSTMASTER; } if (config_get_value("SENDMAIL", "DBMAIL", sendmail) < 0) { TRACE(TRACE_ERR, "error getting value for SENDMAIL in DBMAIL section of dbmail.conf."); return -1; } if (strlen(sendmail) < 1) { TRACE(TRACE_ERR, "SENDMAIL not set in DBMAIL section of dbmail.conf."); return -1; } if (!sendmail_external) { if (parse_and_escape(to, &escaped_to) < 0) { TRACE(TRACE_NOTICE, "could not prepare 'to' address."); return 1; } if (parse_and_escape(from, &escaped_from) < 0) { g_free(escaped_to); TRACE(TRACE_NOTICE, "could not prepare 'from' address."); return 1; } sendmail_command = g_strconcat(sendmail, " -f ", escaped_from, " ", escaped_to, NULL); g_free(escaped_to); g_free(escaped_from); if (!sendmail_command) { TRACE(TRACE_ERR, "out of memory calling g_strconcat"); return -1; } } else { sendmail_command = sendmail_external; } TRACE(TRACE_INFO, "opening pipe to [%s]", sendmail_command); if (!(mailpipe = popen(sendmail_command, "w"))) { TRACE(TRACE_ERR, "could not open pipe to sendmail"); g_free(sendmail_command); return 1; } TRACE(TRACE_DEBUG, "pipe opened"); switch (sendwhat) { case SENDRAW: // This is a hack so forwards can give a From line. if (preoutput) fprintf(mailpipe, "%s\n", preoutput); // This function will dot-stuff the message. db_send_message_lines(mailpipe, message->id, -2, 1); break; case SENDMESSAGE: message_string = dbmail_message_to_string(message); fprintf(mailpipe, "%s", message_string); g_free(message_string); break; default: TRACE(TRACE_ERR, "invalid sendwhat in call to send_mail: [%d]", sendwhat); break; } result = pclose(mailpipe); TRACE(TRACE_DEBUG, "pipe closed"); /* Adapted from the Linux waitpid 2 man page. */ if (WIFEXITED(result)) { result = WEXITSTATUS(result); TRACE(TRACE_INFO, "sendmail exited normally"); } else if (WIFSIGNALED(result)) { result = WTERMSIG(result); TRACE(TRACE_INFO, "sendmail was terminated by signal"); } else if (WIFSTOPPED(result)) { result = WSTOPSIG(result); TRACE(TRACE_INFO, "sendmail was stopped by signal"); } if (result != 0) { TRACE(TRACE_ERR, "sendmail error return value was [%d]", result); if (!sendmail_external) g_free(sendmail_command); return 1; } if (!sendmail_external) g_free(sendmail_command); return 0; } int send_forward_list(DbmailMessage *message, GList *targets, const char *from) { int result = 0; field_t postmaster; if (!from) { if (config_get_value("POSTMASTER", "DBMAIL", postmaster) < 0) TRACE(TRACE_NOTICE, "no config value for POSTMASTER"); if (strlen(postmaster)) from = postmaster; else from = DEFAULT_POSTMASTER; } targets = g_list_first(targets); TRACE(TRACE_INFO, "delivering to [%u] external addresses", g_list_length(targets)); while (targets) { char *to = (char *)targets->data; if (!to || strlen(to) < 1) { TRACE(TRACE_ERR, "forwarding address is zero length, message not forwarded."); } else { if (to[0] == '!') { // The forward is a command to execute. // Prepend an mbox From line. char timestr[50]; time_t td; struct tm tm; char *fromline; time(&td); /* get time */ tm = *localtime(&td); /* get components */ strftime(timestr, sizeof(timestr), "%a %b %e %H:%M:%S %Y", &tm); TRACE(TRACE_DEBUG, "prepending mbox style From header to pipe returnpath: %s", from); /* Format: From<space>address<space><space>Date */ fromline = g_strconcat("From ", from, " ", timestr, NULL); result |= send_mail(message, "", "", fromline, SENDRAW, to+1); g_free(fromline); } else if (to[0] == '|') { // The forward is a command to execute. result |= send_mail(message, "", "", NULL, SENDRAW, to+1); } else { // The forward is an email address. result |= send_mail(message, to, from, NULL, SENDRAW, SENDMAIL); } } if (! g_list_next(targets)) break; targets = g_list_next(targets); } return result; } /* Here's the real *meat* of this source file! * * Function: insert_messages() * What we get: * - The message * - A list of destinations * * What we do: * - Store the message * - Process the destination addresses into lists: * - Local useridnr's * - External forwards * - No such user bounces * - Store the local useridnr's * - Run the message through each user's sorting rules * - Potentially alter the delivery: * - Different mailbox * - Bounce * - Reply with vacation message * - Forward to another address * - Check the user's quota before delivering * - Do this *after* their sorting rules, since the * sorting rules might not store the message anyways * - Send out the no such user bounces * - Send out the external forwards * - Delete the temporary message from the database * What we return: * - 0 on success * - -1 on full failure */ int insert_messages(DbmailMessage *message, GList *dsnusers) { u64_t bodysize, rfcsize; u64_t tmpid; u64_t msgsize; int result=0; delivery_status_t final_dsn; /* first start a new database transaction */ if ((result = dbmail_message_store(message)) == DM_EQUERY) { TRACE(TRACE_ERR,"storing message failed"); return result; } TRACE(TRACE_DEBUG, "temporary msgidnr is [%llu]", message->id); tmpid = message->id; // for later removal bodysize = (u64_t)dbmail_message_get_body_size(message, FALSE); rfcsize = (u64_t)dbmail_message_get_rfcsize(message); msgsize = (u64_t)dbmail_message_get_size(message, FALSE); // TODO: Run a Sieve script associated with the internal delivery user. // Code would go here, after we've stored the message // before we've started delivering it /* Loop through the users list. */ dsnusers = g_list_first(dsnusers); while (dsnusers) { GList *userids; int has_2 = 0, has_4 = 0, has_5 = 0, has_5_2 = 0; deliver_to_user_t *delivery = (deliver_to_user_t *) dsnusers->data; /* Each user may have a list of user_idnr's for local * delivery. */ userids = g_list_first(delivery->userids); while (userids) { u64_t *useridnr = (u64_t *) userids->data; TRACE(TRACE_DEBUG, "calling sort_and_deliver for useridnr [%llu]", *useridnr); switch (sort_and_deliver(message, delivery->address, *useridnr, delivery->mailbox, delivery->source)) { case DSN_CLASS_OK: TRACE(TRACE_INFO, "successful sort_and_deliver for useridnr [%llu]", *useridnr); has_2 = 1; break; case DSN_CLASS_FAIL: TRACE(TRACE_ERR, "permanent failure sort_and_deliver for useridnr [%llu]", *useridnr); has_5 = 1; break; case DSN_CLASS_QUOTA: TRACE(TRACE_NOTICE, "mailbox over quota, message rejected for useridnr [%llu]", *useridnr); has_5_2 = 1; break; case DSN_CLASS_TEMP: default: TRACE(TRACE_ERR, "unknown temporary failure in sort_and_deliver for useridnr [%llu]", *useridnr); has_4 = 1; break; } if (! g_list_next(userids)) break; userids = g_list_next(userids); } final_dsn.class = dsnuser_worstcase_int(has_2, has_4, has_5, has_5_2); switch (final_dsn.class) { case DSN_CLASS_OK: /* Success. Address related. Valid. */ set_dsn(&delivery->dsn, DSN_CLASS_OK, 1, 5); break; case DSN_CLASS_TEMP: /* sort_and_deliver returns TEMP is useridnr is 0, aka, * if nothing was delivered at all, or for any other failures. */ /* If there's a problem with the delivery address, but * there are proper forwarding addresses, we're OK. */ if (g_list_length(delivery->forwards) > 0) { /* Success. Address related. Valid. */ set_dsn(&delivery->dsn, DSN_CLASS_OK, 1, 5); break; } /* Fall through to FAIL. */ case DSN_CLASS_FAIL: /* Permanent failure. Address related. Does not exist. */ set_dsn(&delivery->dsn, DSN_CLASS_FAIL, 1, 1); break; case DSN_CLASS_QUOTA: /* Permanent failure. Mailbox related. Over quota limit. */ set_dsn(&delivery->dsn, DSN_CLASS_FAIL, 2, 2); break; case DSN_CLASS_NONE: /* Leave the DSN status at whatever dsnuser_resolve set it at. */ break; } TRACE(TRACE_DEBUG, "deliver [%u] messages to external addresses", g_list_length(delivery->forwards)); /* Each user may also have a list of external forwarding addresses. */ if (g_list_length(delivery->forwards) > 0) { TRACE(TRACE_DEBUG, "delivering to external addresses"); const char *from = dbmail_message_get_header(message, "Return-Path"); /* Forward using the temporary stored message. */ if (send_forward_list(message, delivery->forwards, from) < 0) { /* If forward fails, tell the sender that we're * having a transient error. They'll resend. */ TRACE(TRACE_NOTICE, "forwaring failed, reporting transient error."); set_dsn(&delivery->dsn, DSN_CLASS_TEMP, 1, 1); } } if (! g_list_next(dsnusers)) break; dsnusers = g_list_next(dsnusers); } /* Always delete the temporary message, even if the delivery failed. * It is the MTA's job to requeue or bounce the message, * and our job to keep a tidy database ;-) */ if (! db_delete_message(tmpid)) TRACE(TRACE_ERR, "failed to delete temporary message [%llu]", message->id); TRACE(TRACE_DEBUG, "temporary message deleted from database. Done."); return 0; }