90f45cfc539cce75e79b5fb6373364f3f05baaf1
[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 <tny-camel-html-to-text-stream.h>
37 #include "modest-formatter.h"
38 #include "modest-text-utils.h"
39 #include "modest-tny-platform-factory.h"
40 #include <modest-runtime.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 guint
72 count_end_tag_lines (const gchar *haystack, const gchar *needle)
73 {
74         gchar *tmp;
75         guint lines = 0;
76
77         tmp = g_strstr_len (haystack, g_utf8_strlen (haystack, -1), ">\n");
78         while (tmp && (tmp <= needle)) {
79                 lines++;
80                 tmp += 2;
81                 tmp = g_strstr_len (tmp, g_utf8_strlen (tmp, -1), ">\n");
82         }
83
84         return lines;
85 }
86
87 static gchar *
88 extract_text (ModestFormatter *self, TnyMimePart *body)
89 {
90         TnyStream *mp_stream;
91         TnyStream *stream;
92         TnyStream *input_stream;
93         GtkTextBuffer *buf;
94         GtkTextIter start, end;
95         gchar *text;
96         ModestFormatterPrivate *priv;
97         gint total, lines, total_lines, line_chars;
98         gboolean is_html, first_time;
99
100         buf = gtk_text_buffer_new (NULL);
101         stream = TNY_STREAM (tny_gtk_text_buffer_stream_new (buf));
102         tny_stream_reset (stream);
103         mp_stream = tny_mime_part_get_decoded_stream (body);
104
105         is_html = (g_strcmp0 (tny_mime_part_get_content_type (body), "text/html") == 0);
106         if (is_html) {
107                 input_stream = tny_camel_html_to_text_stream_new (mp_stream);
108         } else {
109                 input_stream = g_object_ref (mp_stream);
110         }
111
112         total = 0;
113         total_lines = 0;
114         line_chars = 0;
115         lines = 0;
116
117         /* For pure HTML emails tny_camel_html_to_text_stream inserts
118            a \n for every ">\n" found in the email including the HTML
119            headers (<html>, <head> ...). For that reason we need to
120            remove them from the resulting text as it is artificially
121            added by the stream */
122         if (is_html) {
123                 const guint BUFFER_SIZE = 1024;
124                 TnyStream *is;
125                 gboolean look_for_end_tag, found;
126                 gchar buffer [BUFFER_SIZE + 1];
127                 gchar *needle;
128
129                 is = g_object_ref (mp_stream);
130                 look_for_end_tag = FALSE;
131                 found = FALSE;
132
133                 /* This algorithm does not work if the body tag is
134                    spread along 2 different stream reads. But there
135                    are not a lot of changes for this to happen as the
136                    buffer size is big enough in most situations. In
137                    the worst case, when it's not found we just accept
138                    the original translation with the extra "\n" */
139                 while (!tny_stream_is_eos (is) && !found) {
140                         gint n_read;
141
142                         needle = NULL;
143                         memset (buffer, 0, BUFFER_SIZE);
144                         n_read = tny_stream_read (is, buffer, BUFFER_SIZE);
145
146                         if (G_UNLIKELY (n_read < 0))
147                                 break;
148
149                         buffer[n_read] = '\0';
150
151                         /* If we found body,then look for the end of the tag */
152                         if (look_for_end_tag) {
153                                 needle = strchr (buffer, '>');
154
155                                 if (needle) {
156                                         found = TRUE;
157                                         lines += count_end_tag_lines (buffer, needle);
158                                         break;
159                                 }
160                         } else {
161                                 gchar *closing;
162
163                                 /* Try to find the <body> tag. There
164                                    is no other HTML tag starting by
165                                    "bo", and we can detect more cases
166                                    were <body> tag falls into two
167                                    different stream reads */
168                                 needle = g_strstr_len (buffer, n_read, "<bo");
169
170                                 if (needle)
171                                         look_for_end_tag = TRUE;
172                                 else
173                                         needle = &(buffer[n_read]);
174
175                                 lines += count_end_tag_lines (buffer, needle);
176
177                                 closing = strchr (needle, '>');
178                                 if (closing) {
179                                         if (*(closing + 1) == '\n')
180                                                 lines++;
181                                         found = TRUE;
182                                         break;
183                                 }
184                         }
185                 }
186                 if (!found)
187                         lines = 0;
188                 tny_stream_reset (is);
189
190                 g_object_unref (is);
191         }
192
193         first_time = TRUE;
194         while (!tny_stream_is_eos (input_stream)) {
195                 gchar buffer [128];
196                 gchar *offset;
197                 gint n_read;
198                 gint next_read;
199
200                 next_read = MIN (128, MAX_BODY_LENGTH - total);
201                 if (next_read == 0)
202                         break;
203                 n_read = tny_stream_read (input_stream, buffer, next_read);
204
205                 if (G_UNLIKELY (n_read < 0))
206                         break;
207
208                 offset = buffer;
209                 while (offset < buffer + n_read) {
210
211                         if (*offset == '\n') {
212                                 total_lines ++;
213                                 line_chars = 0;
214                         } else {
215                                 line_chars ++;
216                                 if (line_chars >= LINE_WRAP) {
217                                         total_lines ++;
218                                         line_chars = 0;
219                                 }
220                         }
221                         if (total_lines >= MAX_BODY_LINES)
222                                 break;
223                         offset++;
224                 }
225
226                 if (offset - buffer > 0) {
227                         gint n_write = 0, to_write = 0;
228                         gchar *buffer_ptr;
229
230                         /* Discard lines artificially inserted by
231                            Camel when translating from HTML to
232                            text. Do it only for the first read */
233                         buffer_ptr = buffer;
234                         if (G_UNLIKELY (first_time) && lines) {
235                                 int i;
236                                 for (i=0; i < lines; i++) {
237                                         buffer_ptr = strchr (buffer_ptr, '\n');
238                                         buffer_ptr++;
239                                 }
240                                 first_time = FALSE;
241                         }
242                         to_write = offset - buffer_ptr;
243                         n_write = tny_stream_write (stream, buffer_ptr, to_write);
244                         total += n_write;
245                 } else if (n_read == -1) {
246                         break;
247                 }
248
249                 if (total_lines >= MAX_BODY_LINES)
250                         break;
251         }
252
253         tny_stream_reset (stream);
254
255         g_object_unref (G_OBJECT(stream));
256         g_object_unref (G_OBJECT (mp_stream));
257         g_object_unref (G_OBJECT (input_stream));
258
259         gtk_text_buffer_get_bounds (buf, &start, &end);
260         text = gtk_text_buffer_get_text (buf, &start, &end, FALSE);
261         g_object_unref (G_OBJECT(buf));
262
263         /* Convert to desired content type if needed */
264         priv = MODEST_FORMATTER_GET_PRIVATE (self);
265
266         return text;
267 }
268
269 static void
270 construct_from_text (TnyMimePart *part,
271                      const gchar *text,
272                      const gchar *content_type)
273 {
274         TnyStream *text_body_stream;
275
276         /* Create the stream */
277         text_body_stream = TNY_STREAM (tny_camel_mem_stream_new_with_buffer
278                                         (text, strlen(text)));
279
280         /* Construct MIME part */
281         tny_stream_reset (text_body_stream);
282         tny_mime_part_construct (part, text_body_stream, content_type, "7bit");
283         tny_stream_reset (text_body_stream);
284
285         /* Clean */
286         g_object_unref (G_OBJECT (text_body_stream));
287 }
288
289 static TnyMsg *
290 modest_formatter_do (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, FormatterFunc func,
291                      GList *attachments)
292 {
293         TnyMsg *new_msg = NULL;
294         gchar *body_text = NULL, *txt = NULL;
295         ModestFormatterPrivate *priv;
296         TnyMimePart *body_part = NULL;
297
298         g_return_val_if_fail (self, NULL);
299         g_return_val_if_fail (header, NULL);
300         g_return_val_if_fail (func, NULL);
301
302         /* Build new part */
303         new_msg = modest_formatter_create_message (self, TRUE, attachments != NULL, FALSE);
304         body_part = modest_formatter_create_body_part (self, new_msg);
305
306         if (body)
307                 body_text = extract_text (self, body);
308         else
309                 body_text = g_strdup ("");
310
311         txt = (gchar *) func (self, (const gchar*) body_text, header, attachments);
312         priv = MODEST_FORMATTER_GET_PRIVATE (self);
313         construct_from_text (TNY_MIME_PART (body_part), (const gchar*) txt, priv->content_type);
314         g_object_unref (body_part);
315
316         /* Clean */
317         g_free (body_text);
318         g_free (txt);
319
320         return new_msg;
321 }
322
323 TnyMsg *
324 modest_formatter_cite (ModestFormatter *self, TnyMimePart *body, TnyHeader *header)
325 {
326         return modest_formatter_do (self, body, header, modest_formatter_wrapper_cite, NULL);
327 }
328
329 TnyMsg *
330 modest_formatter_quote (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, GList *attachments)
331 {
332         return modest_formatter_do (self, body, header, modest_formatter_wrapper_quote, attachments);
333 }
334
335 TnyMsg *
336 modest_formatter_inline (ModestFormatter *self, TnyMimePart *body, TnyHeader *header, GList *attachments)
337 {
338         return modest_formatter_do (self, body, header, modest_formatter_wrapper_inline, attachments);
339 }
340
341 TnyMsg *
342 modest_formatter_attach (ModestFormatter *self, TnyMsg *msg, TnyHeader *header)
343 {
344         TnyMsg *new_msg = NULL;
345         TnyMimePart *body_part = NULL;
346         ModestFormatterPrivate *priv;
347         gchar *txt;
348
349         /* Build new part */
350         new_msg     = modest_formatter_create_message (self, TRUE, TRUE, FALSE);
351         body_part = modest_formatter_create_body_part (self, new_msg);
352
353         /* Create the two parts */
354         priv = MODEST_FORMATTER_GET_PRIVATE (self);
355         txt = modest_text_utils_cite ("", priv->content_type, priv->signature,
356                                       NULL, tny_header_get_date_sent (header));
357         construct_from_text (body_part, txt, priv->content_type);
358         g_free (txt);
359         g_object_unref (body_part);
360
361         if (msg) {
362                 /* Add parts */
363                 tny_mime_part_add_part (TNY_MIME_PART (new_msg), TNY_MIME_PART (msg));
364         }
365
366         return new_msg;
367 }
368
369 ModestFormatter*
370 modest_formatter_new (const gchar *content_type, const gchar *signature)
371 {
372         ModestFormatter *formatter;
373         ModestFormatterPrivate *priv;
374
375         formatter = g_object_new (MODEST_TYPE_FORMATTER, NULL);
376         priv = MODEST_FORMATTER_GET_PRIVATE (formatter);
377         priv->content_type = g_strdup (content_type);
378         priv->signature = g_strdup (signature);
379
380         return formatter;
381 }
382
383 static void
384 modest_formatter_instance_init (GTypeInstance *instance, gpointer g_class)
385 {
386         ModestFormatter *self = (ModestFormatter *)instance;
387         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
388
389         priv->content_type = NULL;
390         priv->signature = NULL;
391 }
392
393 static void
394 modest_formatter_finalize (GObject *object)
395 {
396         ModestFormatter *self = (ModestFormatter *)object;
397         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
398
399         if (priv->content_type)
400                 g_free (priv->content_type);
401
402         if (priv->signature)
403                 g_free (priv->signature);
404
405         (*parent_class->finalize) (object);
406 }
407
408 static void 
409 modest_formatter_class_init (ModestFormatterClass *class)
410 {
411         GObjectClass *object_class;
412
413         parent_class = g_type_class_peek_parent (class);
414         object_class = (GObjectClass*) class;
415         object_class->finalize = modest_formatter_finalize;
416
417         g_type_class_add_private (object_class, sizeof (ModestFormatterPrivate));
418 }
419
420 GType 
421 modest_formatter_get_type (void)
422 {
423         static GType type = 0;
424
425         if (G_UNLIKELY(type == 0))
426         {
427                 static const GTypeInfo info = 
428                 {
429                   sizeof (ModestFormatterClass),
430                   NULL,   /* base_init */
431                   NULL,   /* base_finalize */
432                   (GClassInitFunc) modest_formatter_class_init,   /* class_init */
433                   NULL,   /* class_finalize */
434                   NULL,   /* class_data */
435                   sizeof (ModestFormatter),
436                   0,      /* n_preallocs */
437                   modest_formatter_instance_init    /* instance_init */
438                 };
439                 
440                 type = g_type_register_static (G_TYPE_OBJECT,
441                         "ModestFormatter",
442                         &info, 0);
443         }
444
445         return type;
446 }
447
448 /****************/
449 static gchar *
450 modest_formatter_wrapper_cite (ModestFormatter *self, const gchar *text, TnyHeader *header,
451                                GList *attachments) 
452 {
453         gchar *result, *from;
454         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
455         
456         from = tny_header_dup_from (header);
457         result = modest_text_utils_cite (text, 
458                                          priv->content_type, 
459                                          priv->signature,
460                                          from, 
461                                          tny_header_get_date_sent (header));
462         g_free (from);
463         return result;
464 }
465
466 static gchar *
467 modest_formatter_wrapper_inline (ModestFormatter *self, const gchar *text, TnyHeader *header,
468                                  GList *attachments) 
469 {
470         gchar *result, *from, *to, *subject;
471         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
472
473         from = tny_header_dup_from (header);
474         to = tny_header_dup_to (header);
475         subject = tny_header_dup_subject (header);
476         result =  modest_text_utils_inline (text, 
477                                             priv->content_type, 
478                                             priv->signature,
479                                             from,
480                                             tny_header_get_date_sent (header),
481                                             to,
482                                             subject);
483         g_free (subject);
484         g_free (to);
485         g_free (from);
486         return result;
487 }
488
489 static gchar *
490 modest_formatter_wrapper_quote (ModestFormatter *self, const gchar *text, TnyHeader *header,
491                                 GList *attachments) 
492 {
493         ModestFormatterPrivate *priv = MODEST_FORMATTER_GET_PRIVATE (self);
494         GList *filenames = NULL;
495         GList *node = NULL;
496         gchar *result = NULL;
497         gchar *from;
498
499         /* First we need a GList of attachments filenames */
500         for (node = attachments; node != NULL; node = g_list_next (node)) {
501                 TnyMimePart *part = (TnyMimePart *) node->data;
502                 gchar *filename = NULL;
503                 if (TNY_IS_MSG (part)) {
504                         TnyHeader *header = tny_msg_get_header (TNY_MSG (part));
505                         filename = tny_header_dup_subject (header);
506                         if ((filename == NULL)||(filename[0] == '\0')) {
507                                 g_free (filename);
508                                 filename = g_strdup (_("mail_va_no_subject"));
509                         }
510                         g_object_unref (header);
511                 } else {
512                         filename = g_strdup (tny_mime_part_get_filename (part));
513                         if ((filename == NULL)||(filename[0] == '\0')) {
514                                 g_free (filename);
515                                 filename = g_strdup ("");
516                         }
517                 }
518                 filenames = g_list_prepend (filenames, filename);
519         }
520
521         /* TODO: get 80 from the configuration */
522         from = tny_header_dup_from (header);
523         result = modest_text_utils_quote (text, 
524                                           priv->content_type, 
525                                           priv->signature,
526                                           from,
527                                           tny_header_get_date_sent (header),
528                                           filenames,
529                                           80);
530         g_free (from);
531
532         g_list_foreach (filenames, (GFunc) g_free, NULL);
533         g_list_free (filenames);
534         return result;
535 }
536
537 TnyMsg * 
538 modest_formatter_create_message (ModestFormatter *self, gboolean single_body, 
539                                  gboolean has_attachments, gboolean has_images)
540 {
541         TnyMsg *result = NULL;
542         TnyPlatformFactory *fact = NULL;
543         TnyMimePart *body_mime_part = NULL;
544         TnyMimePart *related_mime_part = NULL;
545
546         fact    = modest_runtime_get_platform_factory ();
547         result = tny_platform_factory_new_msg (fact);
548         if (has_attachments) {
549                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/mixed");
550                 if (has_images) {
551                         related_mime_part = tny_platform_factory_new_mime_part (fact);
552                         tny_mime_part_set_content_type (related_mime_part, "multipart/related");
553                         tny_mime_part_add_part (TNY_MIME_PART (result), related_mime_part);
554                 } else {
555                         related_mime_part = g_object_ref (result);
556                 }
557                         
558                 if (!single_body) {
559                         body_mime_part = tny_platform_factory_new_mime_part (fact);
560                         tny_mime_part_set_content_type (body_mime_part, "multipart/alternative");
561                         tny_mime_part_add_part (TNY_MIME_PART (related_mime_part), body_mime_part);
562                         g_object_unref (body_mime_part);
563                 }
564
565                 g_object_unref (related_mime_part);
566         } else if (has_images) {
567                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/related");
568
569                 if (!single_body) {
570                         body_mime_part = tny_platform_factory_new_mime_part (fact);
571                         tny_mime_part_set_content_type (body_mime_part, "multipart/alternative");
572                         tny_mime_part_add_part (TNY_MIME_PART (result), body_mime_part);
573                         g_object_unref (body_mime_part);
574                 }
575
576         } else if (!single_body) {
577                 tny_mime_part_set_content_type (TNY_MIME_PART (result), "multipart/alternative");
578         }
579
580         return result;
581 }
582
583 TnyMimePart *
584 find_body_parent (TnyMimePart *part)
585 {
586         const gchar *msg_content_type = NULL;
587         msg_content_type = tny_mime_part_get_content_type (part);
588
589         if ((msg_content_type != NULL) &&
590             (!g_ascii_strcasecmp (msg_content_type, "multipart/alternative")))
591                 return g_object_ref (part);
592         else if ((msg_content_type != NULL) &&
593                  (g_str_has_prefix (msg_content_type, "multipart/"))) {
594                 TnyIterator *iter = NULL;
595                 TnyMimePart *alternative_part = NULL;
596                 TnyMimePart *related_part = NULL;
597                 TnyList *parts = TNY_LIST (tny_simple_list_new ());
598                 tny_mime_part_get_parts (TNY_MIME_PART (part), parts);
599                 iter = tny_list_create_iterator (parts);
600
601                 while (!tny_iterator_is_done (iter)) {
602                         TnyMimePart *part = TNY_MIME_PART (tny_iterator_get_current (iter));
603                         if (part && !g_ascii_strcasecmp(tny_mime_part_get_content_type (part), "multipart/alternative")) {
604                                 alternative_part = part;
605                                 break;
606                         } else if (part && !g_ascii_strcasecmp (tny_mime_part_get_content_type (part), "multipart/related")) {
607                                 related_part = part;
608                                 break;
609                         }
610
611                         if (part)
612                                 g_object_unref (part);
613
614                         tny_iterator_next (iter);
615                 }
616                 g_object_unref (iter);
617                 g_object_unref (parts);
618                 if (related_part) {
619                         TnyMimePart *result;
620                         result = find_body_parent (related_part);
621                         g_object_unref (related_part);
622                         return result;
623                 } else if (alternative_part)
624                         return alternative_part;
625                 else 
626                         return g_object_ref (part);
627         } else
628                 return NULL;
629 }
630
631 TnyMimePart * 
632 modest_formatter_create_body_part (ModestFormatter *self, TnyMsg *msg)
633 {
634         TnyMimePart *result = NULL;
635         TnyPlatformFactory *fact = NULL;
636         TnyMimePart *parent = NULL;
637
638         parent = find_body_parent (TNY_MIME_PART (msg));
639         fact = modest_runtime_get_platform_factory ();
640         if (parent != NULL) {
641                 result = tny_platform_factory_new_mime_part (fact);
642                 tny_mime_part_add_part (TNY_MIME_PART (parent), result);
643                 g_object_unref (parent);
644         } else {
645                 result = g_object_ref (msg);
646         }
647
648         return result;
649
650 }