/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Authors: Jeffrey Stedfast <fejj@helixcode.com>
 *
 *  Copyright 2000 Helix Code, Inc. (www.helixcode.com)
 *
 *  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., 59 Temple Street #330, Boston, MA 02111-1307, USA.
 *
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <ctype.h>
#include <locale.h>

#include "gmime-message.h"
#include "gmime-utils.h"
#include "gmime-stream-mem.h"

static void g_mime_message_destroy (GMimeObject *object);

static GMimeObject object_template = {
	0, 0, g_mime_message_destroy
};

static char *rfc822_headers[] = {
	"Return-Path",
	"Received",
	"Date",
	"From",
	"Reply-To",
	"Subject",
	"Sender",
	"To",
	"Cc",
	NULL
};


/**
 * g_mime_message_new: Create a new MIME Message object
 * @pretty_headers: make pretty headers 
 *
 * If @pretty_headers is %TRUE, then the standard rfc822 headers are
 * initialized so as to put headers in a nice friendly order. This is
 * strictly a cosmetic thing, so if you are unsure, it is safe to say
 * no (%FALSE).
 *
 * Returns an empty MIME Message object.
 **/
GMimeMessage *
g_mime_message_new (gboolean pretty_headers)
{
	GMimeMessage *message;
	GMimeHeader *headers;
	int i;
	
	message = g_new0 (GMimeMessage, 1);
	g_mime_object_construct (GMIME_OBJECT (message),
				 &object_template,
				 GMIME_MESSAGE_TYPE);
	
	message->header = g_new0 (GMimeMessageHeader, 1);
	
	message->header->recipients = g_hash_table_new (g_str_hash, g_str_equal);
	
	message->header->headers = headers = g_mime_header_new ();
	
	if (pretty_headers) {
		/* Populate with the "standard" rfc822 headers so we can have a standard order */
		for (i = 0; rfc822_headers[i]; i++) 
			g_mime_header_set (headers, rfc822_headers[i], NULL);
	}
	
	return message;
}

static gboolean
recipients_destroy (gpointer key, gpointer value, gpointer user_data)
{
	InternetAddressList *recipients = value;
	
	internet_address_list_destroy (recipients);
	
	return TRUE;
}

static void
g_mime_message_destroy (GMimeObject *object)
{
	GMimeMessage *message = (GMimeMessage *) object;
	
	g_return_if_fail (GMIME_IS_MESSAGE (object));
	
	g_free (message->header->from);
	g_free (message->header->reply_to);
	
	/* destroy all recipients */
	g_hash_table_foreach_remove (message->header->recipients, recipients_destroy, NULL);
	g_hash_table_destroy (message->header->recipients);
	
	g_free (message->header->subject);
	
	g_free (message->header->message_id);
	
	g_mime_header_destroy (message->header->headers);
	
	g_free (message->header);
	
	/* unref child mime part */
	if (message->mime_part)
		g_mime_object_unref (GMIME_OBJECT (message->mime_part));
	
	g_free (message);
}


/**
 * g_mime_message_set_sender:
 * @message: MIME Message to change
 * @sender: The name and address of the sender
 *
 * Set the sender's name and address on the MIME Message.
 * (ex: "\"Joe Sixpack\" <joe@sixpack.org>")
 **/
void
g_mime_message_set_sender (GMimeMessage *message, const char *sender)
{
	GMimeMessageHeader *header;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	header = message->header;
	
	if (header->from)
		g_free (header->from);
	
	header->from = g_strstrip (g_strdup (sender));
	g_mime_header_set (header->headers, "From", header->from);
}


/**
 * g_mime_message_get_sender:
 * @message: MIME Message
 *
 * Gets the email address of the sender from @message.
 *
 * Returns the sender's name and address of the MIME Message.
 **/
const char *
g_mime_message_get_sender (GMimeMessage *message)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return message->header->from;
}


/**
 * g_mime_message_set_reply_to:
 * @message: MIME Message to change
 * @reply_to: The Reply-To address
 *
 * Set the sender's Reply-To address on the MIME Message.
 **/
void
g_mime_message_set_reply_to (GMimeMessage *message, const char *reply_to)
{
	GMimeMessageHeader *header;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	header = message->header;
	
	if (header->reply_to)
		g_free (header->reply_to);
	
	header->reply_to = g_strstrip (g_strdup (reply_to));
	g_mime_header_set (header->headers, "Reply-To", header->reply_to);
}


/**
 * g_mime_message_get_reply_to:
 * @message: MIME Message
 *
 * Gets the Reply-To address from @message.
 *
 * Returns the sender's Reply-To address from the MIME Message.
 **/
const char *
g_mime_message_get_reply_to (GMimeMessage *message)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return message->header->reply_to;
}


static void
sync_recipient_header (GMimeMessage *message, const char *type)
{
	InternetAddressList *recipients;
	
	/* sync the specified recipient header */
	recipients = g_mime_message_get_recipients (message, type);
	if (recipients) {
		char *string;
		
		string = internet_address_list_to_string (recipients, TRUE);
		g_mime_header_set (message->header->headers, type, string);
		g_free (string);
	} else
		g_mime_header_set (message->header->headers, type, NULL);
}


/**
 * g_mime_message_add_recipient:
 * @message: MIME Message to change
 * @type: Recipient type
 * @name: The recipient's name
 * @address: The recipient's address
 *
 * Add a recipient of a chosen type to the MIME Message. Available
 * recipient types include: GMIME_RECIPIENT_TYPE_TO,
 * GMIME_RECIPIENT_TYPE_CC and GMIME_RECIPIENT_TYPE_BCC.
 **/
void
g_mime_message_add_recipient (GMimeMessage *message, char *type, const char *name, const char *address)
{
	InternetAddressList *recipients;
	InternetAddress *ia;
	
	ia = internet_address_new_name (name, address);
	
	recipients = g_hash_table_lookup (message->header->recipients, type);
	g_hash_table_remove (message->header->recipients, type);
	
	recipients = internet_address_list_append (recipients, ia);
	internet_address_unref (ia);
	
	g_hash_table_insert (message->header->recipients, type, recipients);
	sync_recipient_header (message, type);
}


/**
 * g_mime_message_add_recipients_from_string:
 * @message: MIME Message
 * @type: Recipient type
 * @string: A string of recipient names and addresses.
 *
 * Add a list of recipients of a chosen type to the MIME
 * Message. Available recipient types include:
 * GMIME_RECIPIENT_TYPE_TO, GMIME_RECIPIENT_TYPE_CC and
 * GMIME_RECIPIENT_TYPE_BCC. The string must be in the format
 * specified in rfc822.
 **/
void
g_mime_message_add_recipients_from_string (GMimeMessage *message, char *type, const char *string)
{
	InternetAddressList *recipients, *addrlist;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (string != NULL);
	
	recipients = g_hash_table_lookup (message->header->recipients, type);
	g_hash_table_remove (message->header->recipients, type);
	
	addrlist = internet_address_parse_string (string);
	if (addrlist) {
		recipients = internet_address_list_concat (recipients, addrlist);
		internet_address_list_destroy (addrlist);
	}
	
	g_hash_table_insert (message->header->recipients, type, recipients);
	
	sync_recipient_header (message, type);
}


/**
 * g_mime_message_get_recipients:
 * @message: MIME Message
 * @type: Recipient type
 *
 * Gets a list of recipients of type @type from @message.
 *
 * Returns a list of recipients of a chosen type from the MIME
 * Message. Available recipient types include:
 * GMIME_RECIPIENT_TYPE_TO, GMIME_RECIPIENT_TYPE_CC and
 * GMIME_RECIPIENT_TYPE_BCC.
 **/
InternetAddressList *
g_mime_message_get_recipients (GMimeMessage *message, const char *type)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return g_hash_table_lookup (message->header->recipients, type);
}


/**
 * g_mime_message_set_subject:
 * @message: MIME Message
 * @subject: Subject string
 *
 * Set the Subject field on a MIME Message.
 **/
void
g_mime_message_set_subject (GMimeMessage *message, const char *subject)
{
	GMimeMessageHeader *header;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	header = message->header;
	
	if (header->subject)
		g_free (header->subject);
	
	header->subject = g_strstrip (g_strdup (subject));
	g_mime_header_set (header->headers, "Subject", header->subject);
}


/**
 * g_mime_message_get_subject:
 * @message: MIME Message
 *
 * Gets the message's subject.
 *
 * Returns the Subject field on a MIME Message.
 **/
const char *
g_mime_message_get_subject (GMimeMessage *message)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return message->header->subject;
}


/**
 * g_mime_message_set_date:
 * @message: MIME Message
 * @date: Sent-date (ex: gotten from time (NULL);)
 * @gmt_offset: GMT date offset (in +/- hours)
 * 
 * Set the sent-date on a MIME Message.
 **/
void
g_mime_message_set_date (GMimeMessage *message, time_t date, int gmt_offset)
{
	char *date_str;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	message->header->date = date;
	message->header->gmt_offset = gmt_offset;
	
	date_str = g_mime_message_get_date_string (message);
	g_mime_header_set (message->header->headers, "Date", date_str);
	g_free (date_str);
}


/**
 * g_mime_message_get_date:
 * @message: MIME Message
 * @date: Sent-date
 * @gmt_offset: GMT date offset (in +/- hours)
 * 
 * Stores the date in time_t format in #date and the GMT offset in
 * @gmt_offset.
 **/
void
g_mime_message_get_date (GMimeMessage *message, time_t *date, int *gmt_offset)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	*date = message->header->date;
	*gmt_offset = message->header->gmt_offset;
}


/**
 * g_mime_message_get_date_string:
 * @message: MIME Message
 *
 * Gets the message's sent date in string format.
 * 
 * Returns the sent-date of the MIME Message in string format.
 **/
char *
g_mime_message_get_date_string (GMimeMessage *message)
{
	char *date_str;
	char *locale;
	
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	locale = g_strdup (setlocale (LC_TIME, ""));
	setlocale (LC_TIME, "POSIX");
	
	date_str = g_mime_utils_header_format_date (message->header->date,
						    message->header->gmt_offset);
	
	if (locale != NULL)
		setlocale (LC_TIME, locale);
	g_free (locale);
	
	return date_str;
}


/**
 * g_mime_message_set_message_id: 
 * @message: MIME Message
 * @id: message-id
 *
 * Set the Message-Id on a message.
 **/
void
g_mime_message_set_message_id (GMimeMessage *message, const char *id)
{
	GMimeMessageHeader *header;
	
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	
	header = message->header;
	
	if (header->message_id)
		g_free (header->message_id);
	
	header->message_id = g_strstrip (g_strdup (id));
	g_mime_header_set (header->headers, "Message-Id", header->message_id);
}


/**
 * g_mime_message_get_message_id: 
 * @message: MIME Message
 *
 * Gets the Message-Id header of @message.
 *
 * Returns the Message-Id of a message.
 **/
const char *
g_mime_message_get_message_id (GMimeMessage *message)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return message->header->message_id;
}


/**
 * g_mime_message_add_header: Add an arbitrary message header
 * @message: MIME Message
 * @header: rfc822 header field
 * @value: the contents of the header field
 *
 * Add an arbitrary message header to the MIME Message such as X-Mailer,
 * X-Priority, or In-Reply-To.
 **/
void
g_mime_message_add_header (GMimeMessage *message, const char *header, const char *value)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (header != NULL);
	
	g_mime_header_add (message->header->headers, header, value);
}


/**
 * g_mime_message_set_header: Add an arbitrary message header
 * @message: MIME Message
 * @header: rfc822 header field
 * @value: the contents of the header field
 *
 * Set an arbitrary message header to the MIME Message such as X-Mailer,
 * X-Priority, or In-Reply-To.
 **/
void
g_mime_message_set_header (GMimeMessage *message, const char *header, const char *value)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (header != NULL);
	
	g_mime_header_set (message->header->headers, header, value);
}


/**
 * g_mime_message_get_header:
 * @message: MIME Message
 * @header: rfc822 header field
 *
 * Gets the value of the requested header @header if it exists, or
 * %NULL otherwise.
 *
 * Returns the value of the requested header (or %NULL if it isn't set)
 **/
const char *
g_mime_message_get_header (GMimeMessage *message, const char *header)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	g_return_val_if_fail (header != NULL, NULL);
	
	return g_mime_header_get (message->header->headers, header);
}


/**
 * g_mime_message_set_mime_part:
 * @message: MIME Message
 * @mime_part: The root-level MIME Part
 *
 * Set the root-level MIME part of the message.
 **/
void
g_mime_message_set_mime_part (GMimeMessage *message, GMimePart *mime_part)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (GMIME_IS_PART (mime_part));
	
	g_mime_object_ref (GMIME_OBJECT (mime_part));
	
	if (message->mime_part)
		g_mime_object_unref (GMIME_OBJECT (message->mime_part));
	
	message->mime_part = mime_part;
}


/**
 * g_mime_message_write_to_stream:
 * @message: MIME Message
 * @stream: output stream
 *
 * Write the contents of the MIME Message to @stream.
 **/
void
g_mime_message_write_to_stream (GMimeMessage *message, GMimeStream *stream)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (stream != NULL);
	
	g_mime_header_write_to_stream (message->header->headers, stream);
	
	if (message->mime_part) {
		g_mime_stream_write_string (stream, "MIME-Version: 1.0\n");
		g_mime_part_write_to_stream (message->mime_part, stream);
	} else
		g_mime_stream_write (stream, "\n", 1);
}


/**
 * g_mime_message_to_string:
 * @message: MIME Message
 *
 * Allocates a string buffer containing the mime message @message.
 *
 * Returns an allocated string containing the MIME Message.
 **/
char *
g_mime_message_to_string (GMimeMessage *message)
{
	GMimeStream *stream;
	GByteArray *array;
	char *str;
	
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	array = g_byte_array_new ();
	stream = g_mime_stream_mem_new ();
	g_mime_stream_mem_set_byte_array (GMIME_STREAM_MEM (stream), array);
	g_mime_message_write_to_stream (message, stream);
	g_mime_stream_unref (stream);
	g_byte_array_append (array, "", 1);
	str = array->data;
	g_byte_array_free (array, FALSE);
	
	return str;
}


/* Brief explanation of how this function works it's magic:
 *
 * We cycle through the immediate subparts looking for text parts. If
 * the first text part we come accross is exactly what we want then we
 * return it, otherwise keep a reference to it for later use (if we
 * don't find the preferred part later as we continue to cycle through
 * the subparts then we default to the first text part found). If we
 * come to a multipart, we descend into it repeating the process. If
 * we find the 'body' in a sub-multipart, we don't necessarily return
 * that value for it is entirely possible that there could be text
 * parts defined after the sub-multipart. For example, we could have
 * the following MIME structure:
 *
 * multipart/alternative
 *   image/png
 *   multipart/related
 *     text/html
 *     image/png
 *     image/gif
 *     image/jpeg
 *   text/plain
 *   text/html
 *
 * While one can never be certain that the text/html part within the
 * multipart/related isn't the true 'body', it's genrally safe to
 * assume that in cases like this, the outer text part(s) are the
 * message body. Note that this is an assumption and is thus not
 * guarenteed to always be correct.
 **/
static char *
multipart_get_body (GMimePart *multipart, gboolean want_plain, gboolean *is_html)
{
	GMimePart *first = NULL;
	const char *content;
	char *body = NULL;
	GList *child;
	size_t len;
	
	child = multipart->children;
	while (child) {
		const GMimeContentType *type;
		GMimePart *mime_part;
		
		mime_part = child->data;
		type = g_mime_part_get_content_type (mime_part);
		
		if (g_mime_content_type_is_type (type, "text", want_plain ? "plain" : "html")) {
			/* we got what we came for */
			*is_html = !want_plain;
			
			content = g_mime_part_get_content (mime_part, &len);
			g_free (body);
			body = g_strndup (content, len);
			break;
		} else if (g_mime_content_type_is_type (type, "text", "*") && !first) {
			/* remember what our first text part was */
			first = mime_part;
			g_free (body);
			body = NULL;
		} else if (g_mime_content_type_is_type (type, "multipart", "*") && !first && !body) {
			/* look in the multipart for the body */
			body = multipart_get_body (mime_part, want_plain, is_html);
			
			/* You are probably asking: "why don't we break here?"
			 * The answer is because the real message body could
			 * be a part after this multipart */
		}
		
		child = child->next;
	}
	
	if (!body && first) {
		/* we didn't get the type we wanted but still got the body */
		*is_html = want_plain;
		
		content = g_mime_part_get_content (first, &len);
		body = g_strndup (content, len);
	}
	
	return body;
}


/**
 * g_mime_message_get_body:
 * @message: MIME Message
 * @want_plain: request text/plain
 * @is_html: body returned is in html format
 *
 * Attempts to get the body of the message in the preferred format
 * specified by @want_plain.
 *
 * Returns the prefered form of the message body. Sets the value of
 * @is_html to TRUE if the part returned is in HTML format, otherwise
 * FALSE.
 * Note: This function is NOT guarenteed to always work as it
 * makes some assumptions that are not necessarily true. It is
 * recommended that you traverse the MIME structure yourself.
 **/
char *
g_mime_message_get_body (const GMimeMessage *message, gboolean want_plain, gboolean *is_html)
{
	const GMimeContentType *type;
	const char *content;
	char *body = NULL;
	size_t len = 0;
	
	g_return_val_if_fail (message!=NULL, NULL);
	g_return_val_if_fail (is_html!=NULL, NULL);
	
	type = g_mime_part_get_content_type (message->mime_part);
	if (g_mime_content_type_is_type (type, "text", "*")) {
		/* this *has* to be the message body */
		if (g_mime_content_type_is_type (type, "text", want_plain ? "plain" : "html"))
			*is_html = !want_plain;
		else
			*is_html = want_plain;
		
		content = g_mime_part_get_content (message->mime_part, &len);
		body = g_strndup (content, len);
	} else if (g_mime_content_type_is_type (type, "multipart", "*")) {
		/* lets see if we can find a body in the multipart */
		body = multipart_get_body (message->mime_part, want_plain, is_html);
	}
	
	return body;
}


/**
 * g_mime_message_get_headers:
 * @message: MIME Message
 *
 * Allocates a string buffer containing the raw message headers.
 *
 * Returns an allocated string containing the raw message headers.
 **/
char *
g_mime_message_get_headers (GMimeMessage *message)
{
	g_return_val_if_fail (GMIME_IS_MESSAGE (message), NULL);
	
	return g_mime_header_to_string (message->header->headers);
}


/**
 * g_mime_message_foreach_part:
 * @message: MIME message
 * @callback: function to call on each of the mime parts contained by the mime message
 * @data: extra data to pass to the callback
 *
 * Calls @callback on each of the mime parts in the mime message.
 **/
void
g_mime_message_foreach_part (GMimeMessage *message, GMimePartFunc callback, gpointer data)
{
	g_return_if_fail (GMIME_IS_MESSAGE (message));
	g_return_if_fail (callback != NULL);
	
	g_mime_part_foreach (message->mime_part, callback, data);
}


syntax highlighted by Code2HTML, v. 0.9.1