Properly reply to pure HTML emails.
[modest] / src / modest-formatter.c
1 /* Copyright (c) 2006, Nokia Corporation
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  * * Redistributions of source code must retain the above copyright
9  *   notice, this list of conditions and the following disclaimer.
10  * * Redistributions in binary form must reproduce the above copyright
11  *   notice, this list of conditions and the following disclaimer in the
12  *   documentation and/or other materials provided with the distribution.
13  * * Neither the name of the Nokia Corporation nor the names of its
14  *   contributors may be used to endorse or promote products derived from
15  *   this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
18  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #include <glib/gi18n.h>
31 #include <string.h>
32 #include <tny-header.h>
33 #include <tny-simple-list.h>
34 #include <tny-gtk-text-buffer-stream.h>
35 #include <tny-camel-mem-stream.h>
36 #include "modest-formatter.h"
37 #include "modest-text-utils.h"
38 #include "modest-tny-platform-factory.h"
39 #include "modest-runtime.h"
40 #include "modest-stream-html-to-text.h"
41
42 #define LINE_WRAP 78
43 #define MAX_BODY_LINES 1024
44 #define MAX_BODY_LENGTH 1024*128
45
46 typedef struct _ModestFormatterPrivate ModestFormatterPrivate;
47 struct _ModestFormatterPrivate {
48         gchar *content_type;
49         gchar *signature;
50 };
51 #define MODEST_FORMATTER_GET_PRIVATE(o)  (G_TYPE_INSTANCE_GET_PRIVATE((o), \
52                                           MODEST_TYPE_FORMATTER, \
53                                           ModestFormatterPrivate))
54
55 static GObjectClass *parent_class = NULL;
56
57 typedef gchar* FormatterFunc (ModestFormatter *self, const gchar *text, TnyHeader *header, GList *attachments);
58
59 static TnyMsg *modest_formatter_do (ModestFormatter *self, TnyMimePart *body,  TnyHeader *header, 
60                                     FormatterFunc func, GList *attachments);
61
62 static gchar*  modest_formatter_wrapper_cite   (ModestFormatter *self, const gchar *text,
63                                                 TnyHeader *header, GList *attachments);
64 static gchar*  modest_formatter_wrapper_quote  (ModestFormatter *self, const gchar *text,
65                                                 TnyHeader *header, GList *attachments);
66 static gchar*  modest_formatter_wrapper_inline (ModestFormatter *self, const gchar *text,
67                                                 TnyHeader *header, GList *attachments);
68
69 static TnyMimePart *find_body_parent (TnyMimePart *part);
70
71 static gchar *
72 extract_text (ModestFormatter *self, TnyMimePart *body)
73 {
74         TnyStream *mp_stream;
75         TnyStream *stream;
76         TnyStream *input_stream;
77         GtkTextBuffer *buf;
78         GtkTextIter start, end;
79         gchar *text;
80         ModestFormatterPrivate *priv;
81         gint total, lines, total_lines, line_chars;
82         gboolean is_html, first_time;
83
84         buf = gtk_text_buffer_new (NULL);
85         stream = TNY_STREAM (tny_gtk_text_buffer_stream_new (buf));
86         tny_stream_reset (stream);
87         mp_stream = tny_mime_part_get_decoded_stream (body);
88
89         is_html = (g_strcmp0 (tny_mime_part_get_content_type (body), "text/html") == 0);
90         if (is_html) {
91                 input_stream = modest_stream_html_to_text_new (mp_stream);
92         } else {
93                 input_stream = g_object_ref (mp_stream);
94         }
95
96         total = 0;
97         total_lines = 0;
98         line_chars = 0;
99         lines = 0;
100
101         first_time = TRUE;
102         while (!tny_stream_is_eos (input_stream)) {
103                 gchar buffer [128];
104                 gchar *offset;
105                 gint n_read;
106                 gint next_read;
107
108                 next_read = MIN (128, MAX_BODY_LENGTH - total);
109                 if (next_read == 0)
110                         break;
111                 n_read = tny_stream_read (input_stream, buffer, next_read);
112
113                 if (G_UNLIKELY (n_read < 0))
114                         break;
115
116                 offset = buffer;
117                 while (offset < buffer + n_read) {
118
119                         if (*offset == '\n') {
120                                 total_lines ++;
121                                 line_chars = 0;
122                         } else {
123                                 line_chars ++;
124                                 if (line_chars >= LINE_WRAP) {
125                                         total_lines ++;
126                                         line_chars = 0;
127                                 }
128                         }
129                         if (total_lines >= MAX_BODY_LINES)
130                                 break;
131                         offset++;
132                 }
133
134                 if (offset - buffer > 0) {
135                         gint n_write = 0, to_write = 0;
136
137                         to_write = offset - buffer;
138                         n_write = tny_stream_write (stream, buffer, to_write);
139                         total += n_write;
140                 } else if (n_read == -1) {
141                         break;
142                 }
143
144                 if (total_lines >= MAX_BODY_LINES)
145                         break;
146         }
147
148         tny_stream_reset (stream);
149
150         g_object_unref (G_OBJECT(stream));
151         g_object_unref (G_OBJECT (mp_stream));
152         g_object_unref (G_OBJECT (input_stream));
153
154         gtk_text_buffer_get_bounds (buf, &start, &end);
155         text = gtk_text_buffer_get_text (buf, &start, &end, FALSE);
156         g_object_unref (G_OBJECT(buf));
157
158         /* Convert to desired content type if needed */
159         priv = MODEST_FORMATTER_GET_PRIVATE (self);
160
161         return text;
162 }
163
164 static void
165 construct_from_text (TnyMimePart *part,
166                      const gchar *text,
167                      const gchar *content_type)
168 {
169         TnyStream *text_body_stream;
170
171         /* Create the stream */
172         text_body_stream = TNY_STREAM (tny_camel_mem_stream_new_with_buffer
173                                         (text, strlen(text)));
174
175         /* Construct MIME part */
176         tny_stream_reset (text_body_stream);
177         tny_mime_part_construct (part, text_body_stream, content_type, "7bit");
178         tny_stream_reset (text_body_stream);
179
180         /* Clean */
181         g_object_unref (G_OBJECT (text_body_stream));
182 }
183
184 static TnyMsg *
185 modest_formatter_do (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, FormatterFunc func,
186                      GList *attachments)
187 {
188         TnyMsg *new_msg = NULL;
189         gchar *body_text = NULL, *txt = NULL;
190         ModestFormatterPrivate *priv;
191         TnyMimePart *body_part = NULL;
192
193         g_return_val_if_fail (self, NULL);
194         g_return_val_if_fail (header, NULL);
195         g_return_val_if_fail (func, NULL);
196
197         /* Build new part */
198         new_msg = modest_formatter_create_message (self, TRUE, attachments != NULL, FALSE);
199         body_part = modest_formatter_create_body_part (self, new_msg);
200
201         if (body)
202                 body_text = extract_text (self, body);
203         else
204                 body_text = g_strdup ("");
205
206         txt = (gchar *) func (self, (const gchar*) body_text, header, attachments);
207         priv = MODEST_FORMATTER_GET_PRIVATE (self);
208         construct_from_text (TNY_MIME_PART (body_part), (const gchar*) txt, priv->content_type);
209         g_object_unref (body_part);
210
211         /* Clean */
212         g_free (body_text);
213         g_free (txt);
214
215         return new_msg;
216 }
217
218 TnyMsg *
219 modest_formatter_cite (ModestFormatter *self, TnyMimePart *body, TnyHeader *header)
220 {
221         return modest_formatter_do (self, body, header, modest_formatter_wrapper_cite, NULL);
222 }
223
224 TnyMsg *
225 modest_formatter_quote (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, GList *attachments)
226 {
227         return modest_formatter_do (self, body, header, modest_formatter_wrapper_quote, attachments);
228 }
229
230 TnyMsg *
231 modest_formatter_inline (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, GList *attachments)
232 {
233         return modest_formatter_do (self, body, header, modest_formatter_wrapper_inline, attachments);
234 }
235
236 TnyMsg *
237 modest_formatter_attach (ModestFormatter *self, TnyMsg *msg, TnyHeader *header)
238 {
239         TnyMsg *new_msg = NULL;
240         TnyMimePart *body_part = NULL;
241         ModestFormatterPrivate *priv;
242         gchar *txt;
243
244         /* Build new part */
245         new_msg     = modest_formatter_create_message (self, TRUE, TRUE, FALSE);
246         body_part = modest_formatter_create_body_part (self, new_msg);
247
248         /* Create the two parts */
249         priv = MODEST_FORMATTER_GET_PRIVATE (self);
250         txt = modest_text_utils_cite ("", priv->content_type, priv->signature,
251                                       NULL, tny_header_get_date_sent (header));
252         construct_from_text (body_part, txt, priv->content_type);
253         g_free (txt);
254         g_object_unref (body_part);
255
256         if (msg) {
257                 /* Add parts */
258                 tny_mime_part_add_part (TNY_MIME_PART (new_msg), TNY_MIME_PART (msg));
259         }
260
261         return new_msg;
262 }
263
264 ModestFormatter*
265 modest_formatter_new (const gchar *content_type, const gchar *signature)
266 {
267         ModestFormatter *formatter;
268         ModestFormatterPrivate *priv;
269
270         formatter = g_object_new (MODEST_TYPE_FORMATTER, NULL);
271         priv = MODEST_FORMATTER_GET_PRIVATE (formatter);
272         priv->content_type = g_strdup (content_type);
273         priv->signature = g_strdup (signature);
274
275         return formatter;
276 }
277
278 static void
279 modest_formatter_instance_init (GTypeInstance *instance, gpointer g_class)
280 {
281         ModestFormatter *self = (ModestFormatter *)instance;
282         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
283
284         priv->content_type = NULL;
285         priv->signature = NULL;
286 }
287
288 static void
289 modest_formatter_finalize (GObject *object)
290 {
291         ModestFormatter *self = (ModestFormatter *)object;
292         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
293
294         if (priv->content_type)
295                 g_free (priv->content_type);
296
297         if (priv->signature)
298                 g_free (priv->signature);
299
300         (*parent_class->finalize) (object);
301 }
302
303 static void 
304 modest_formatter_class_init (ModestFormatterClass *class)
305 {
306         GObjectClass *object_class;
307
308         parent_class = g_type_class_peek_parent (class);
309         object_class = (GObjectClass*) class;
310         object_class->finalize = modest_formatter_finalize;
311
312         g_type_class_add_private (object_class, sizeof (ModestFormatterPrivate));
313 }
314
315 GType 
316 modest_formatter_get_type (void)
317 {
318         static GType type = 0;
319
320         if (G_UNLIKELY(type == 0))
321         {
322                 static const GTypeInfo info = 
323                 {
324                   sizeof (ModestFormatterClass),
325                   NULL,   /* base_init */
326                   NULL,   /* base_finalize */
327                   (GClassInitFunc) modest_formatter_class_init,   /* class_init */
328                   NULL,   /* class_finalize */
329                   NULL,   /* class_data */
330                   sizeof (ModestFormatter),
331                   0,      /* n_preallocs */
332                   modest_formatter_instance_init    /* instance_init */
333                 };
334                 
335                 type = g_type_register_static (G_TYPE_OBJECT,
336                         "ModestFormatter",
337                         &info, 0);
338         }
339
340         return type;
341 }
342
343 /****************/
344 static gchar *
345 modest_formatter_wrapper_cite (ModestFormatter *self, const gchar *text, TnyHeader *header,
346                                GList *attachments) 
347 {
348         gchar *result, *from;
349         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
350         
351         from = tny_header_dup_from (header);
352         result = modest_text_utils_cite (text, 
353                                          priv->content_type, 
354                                          priv->signature,
355                                          from, 
356                                          tny_header_get_date_sent (header));
357         g_free (from);
358         return result;
359 }
360
361 static gchar *
362 modest_formatter_wrapper_inline (ModestFormatter *self, const gchar *text, TnyHeader *header,
363                                  GList *attachments) 
364 {
365         gchar *result, *from, *to, *subject;
366         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
367
368         from = tny_header_dup_from (header);
369         to = tny_header_dup_to (header);
370         subject = tny_header_dup_subject (header);
371         result =  modest_text_utils_inline (text, 
372                                             priv->content_type, 
373                                             priv->signature,
374                                             from,
375                                             tny_header_get_date_sent (header),
376                                             to,
377                                             subject);
378         g_free (subject);
379         g_free (to);
380         g_free (from);
381         return result;
382 }
383
384 static gchar *
385 modest_formatter_wrapper_quote (ModestFormatter *self, const gchar *text, TnyHeader *header,
386                                 GList *attachments) 
387 {
388         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
389         GList *filenames = NULL;
390         GList *node = NULL;
391         gchar *result = NULL;
392         gchar *from;
393
394         /* First we need a GList of attachments filenames */
395         for (node = attachments; node != NULL; node = g_list_next (node)) {
396                 TnyMimePart *part = (TnyMimePart *) node->data;
397                 gchar *filename = NULL;
398                 if (TNY_IS_MSG (part)) {
399                         TnyHeader *header = tny_msg_get_header (TNY_MSG (part));
400                         filename = tny_header_dup_subject (header);
401                         if ((filename == NULL)||(filename[0] == '\0')) {
402                                 g_free (filename);
403                                 filename = g_strdup (_("mail_va_no_subject"));
404                         }
405                         g_object_unref (header);
406                 } else {
407                         filename = g_strdup (tny_mime_part_get_filename (part));
408                         if ((filename == NULL)||(filename[0] == '\0')) {
409                                 g_free (filename);
410                                 filename = g_strdup ("");
411                         }
412                 }
413                 filenames = g_list_prepend (filenames, filename);
414         }
415
416         /* TODO: get 80 from the configuration */
417         from = tny_header_dup_from (header);
418         result = modest_text_utils_quote (text, 
419                                           priv->content_type, 
420                                           priv->signature,
421                                           from,
422                                           tny_header_get_date_sent (header),
423                                           filenames,
424                                           80);
425         g_free (from);
426
427         g_list_foreach (filenames, (GFunc) g_free, NULL);
428         g_list_free (filenames);
429         return result;
430 }
431
432 TnyMsg * 
433 modest_formatter_create_message (ModestFormatter *self, gboolean single_body, 
434                                  gboolean has_attachments, gboolean has_images)
435 {
436         TnyMsg *result = NULL;
437         TnyPlatformFactory *fact = NULL;
438         TnyMimePart *body_mime_part = NULL;
439         TnyMimePart *related_mime_part = NULL;
440
441         fact    = modest_runtime_get_platform_factory ();
442         result = tny_platform_factory_new_msg (fact);
443         if (has_attachments) {
444                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/mixed");
445                 if (has_images) {
446                         related_mime_part = tny_platform_factory_new_mime_part (fact);
447                         tny_mime_part_set_content_type (related_mime_part, "multipart/related");
448                         tny_mime_part_add_part (TNY_MIME_PART (result), related_mime_part);
449                 } else {
450                         related_mime_part = g_object_ref (result);
451                 }
452                         
453                 if (!single_body) {
454                         body_mime_part = tny_platform_factory_new_mime_part (fact);
455                         tny_mime_part_set_content_type (body_mime_part, "multipart/alternative");
456                         tny_mime_part_add_part (TNY_MIME_PART (related_mime_part), body_mime_part);
457                         g_object_unref (body_mime_part);
458                 }
459
460                 g_object_unref (related_mime_part);
461         } else if (has_images) {
462                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/related");
463
464                 if (!single_body) {
465                         body_mime_part = tny_platform_factory_new_mime_part (fact);
466                         tny_mime_part_set_content_type (body_mime_part, "multipart/alternative");
467                         tny_mime_part_add_part (TNY_MIME_PART (result), body_mime_part);
468                         g_object_unref (body_mime_part);
469                 }
470
471         } else if (!single_body) {
472                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/alternative");
473         }
474
475         return result;
476 }
477
478 TnyMimePart *
479 find_body_parent (TnyMimePart *part)
480 {
481         const gchar *msg_content_type = NULL;
482         msg_content_type = tny_mime_part_get_content_type (part);
483
484         if ((msg_content_type != NULL) &&
485             (!g_ascii_strcasecmp (msg_content_type, "multipart/alternative")))
486                 return g_object_ref (part);
487         else if ((msg_content_type != NULL) &&
488                  (g_str_has_prefix (msg_content_type, "multipart/"))) {
489                 TnyIterator *iter = NULL;
490                 TnyMimePart *alternative_part = NULL;
491                 TnyMimePart *related_part = NULL;
492                 TnyList *parts = TNY_LIST (tny_simple_list_new ());
493                 tny_mime_part_get_parts (TNY_MIME_PART (part), parts);
494                 iter = tny_list_create_iterator (parts);
495
496                 while (!tny_iterator_is_done (iter)) {
497                         TnyMimePart *part = TNY_MIME_PART (tny_iterator_get_current (iter));
498                         if (part && !g_ascii_strcasecmp(tny_mime_part_get_content_type (part), "multipart/alternative")) {
499                                 alternative_part = part;
500                                 break;
501                         } else if (part && !g_ascii_strcasecmp (tny_mime_part_get_content_type (part), "multipart/related")) {
502                                 related_part = part;
503                                 break;
504                         }
505
506                         if (part)
507                                 g_object_unref (part);
508
509                         tny_iterator_next (iter);
510                 }
511                 g_object_unref (iter);
512                 g_object_unref (parts);
513                 if (related_part) {
514                         TnyMimePart *result;
515                         result = find_body_parent (related_part);
516                         g_object_unref (related_part);
517                         return result;
518                 } else if (alternative_part)
519                         return alternative_part;
520                 else 
521                         return g_object_ref (part);
522         } else
523                 return NULL;
524 }
525
526 TnyMimePart * 
527 modest_formatter_create_body_part (ModestFormatter *self, TnyMsg *msg)
528 {
529         TnyMimePart *result = NULL;
530         TnyPlatformFactory *fact = NULL;
531         TnyMimePart *parent = NULL;
532
533         parent = find_body_parent (TNY_MIME_PART (msg));
534         fact = modest_runtime_get_platform_factory ();
535         if (parent != NULL) {
536                 result = tny_platform_factory_new_mime_part (fact);
537                 tny_mime_part_add_part (TNY_MIME_PART (parent), result);
538                 g_object_unref (parent);
539         } else {
540                 result = g_object_ref (msg);
541         }
542
543         return result;
544
545 }