130bc8f9eaff1e49bacc3a8a0923f1330e316dff
[modest] / src / modest-text-utils.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
31 #include <glib.h>
32 #include <string.h>
33 #include <stdlib.h>
34 #include <glib/gi18n.h>
35 #include <regex.h>
36 #include "modest-text-utils.h"
37
38
39 #ifdef HAVE_CONFIG_H
40 #include <config.h>
41 #endif /*HAVE_CONFIG_H */
42
43 /* defines */
44 #define FORWARD_STRING _("-----Forwarded Message-----")
45 #define FROM_STRING _("From:")
46 #define SENT_STRING _("Sent:")
47 #define TO_STRING _("To:")
48 #define SUBJECT_STRING _("Subject:")
49
50 /*
51  * we need these regexps to find URLs in plain text e-mails
52  */
53 typedef struct _url_match_pattern_t url_match_pattern_t;
54 struct _url_match_pattern_t {
55         gchar   *regex;
56         regex_t *preg;
57         gchar   *prefix;
58 };
59
60 typedef struct _url_match_t url_match_t;
61 struct _url_match_t {
62         guint offset;
63         guint len;
64         const gchar* prefix;
65 };
66
67 #define MAIL_VIEWER_URL_MATCH_PATTERNS  {                               \
68         { "(file|rtsp|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\
69           NULL, NULL },\
70         { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
71           NULL, "http://" },\
72         { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
73           NULL, "ftp://" },\
74         { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \
75            NULL, NULL},                                             \
76         { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+",                    \
77           NULL, NULL},\
78         { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
79           NULL, "mailto:"}\
80         }
81
82 /* private */
83 static gchar*   cite                    (const time_t sent_date, const gchar *from);
84 static void     hyperlinkify_plain_text (GString *txt);
85 static gint     cmp_offsets_reverse     (const url_match_t *match1, const url_match_t *match2);
86 static void     chk_partial_match       (const url_match_t *match, guint* offset);
87 static GSList*  get_url_matches         (GString *txt);
88
89 static GString* get_next_line           (const char *b, const gsize blen, const gchar * iter);
90 static int      get_indent_level        (const char *l);
91 static void     unquote_line            (GString * l);
92 static void     append_quoted           (GString * buf, const int indent, const GString * str, 
93                                          const int cutpoint);
94 static int      get_breakpoint_utf8     (const gchar * s, const gint indent, const gint limit);
95 static int      get_breakpoint_ascii    (const gchar * s, const gint indent, const gint limit);
96 static int      get_breakpoint          (const gchar * s, const gint indent, const gint limit);
97
98 static gchar*   modest_text_utils_quote_plain_text (const gchar *text, 
99                                                     const gchar *cite, 
100                                                     int limit);
101
102 static gchar*   modest_text_utils_quote_html       (const gchar *text, 
103                                                     const gchar *cite, 
104                                                     int limit);
105
106
107 /* ******************************************************************* */
108 /* ************************* PUBLIC FUNCTIONS ************************ */
109 /* ******************************************************************* */
110
111 gchar *
112 modest_text_utils_quote (const gchar *text, 
113                          const gchar *content_type,
114                          const gchar *from,
115                          const time_t sent_date, 
116                          int limit)
117 {
118         gchar *retval, *cited;
119
120         cited = cite (sent_date, from);
121
122         if (!strcmp (content_type, "text/html"))
123                 /* TODO: extract the <body> of the HTML and pass it to
124                    the function */
125                 retval = modest_text_utils_quote_html (text, cited, limit);
126         else
127                 retval = modest_text_utils_quote_plain_text (text, cited, limit);
128                 
129         g_free (cited);
130
131         return retval;
132 }
133
134
135 gchar *
136 modest_text_utils_cite (const gchar *text,
137                         const gchar *content_type,
138                         const gchar *from,
139                         time_t sent_date)
140 {
141         gchar *tmp, *retval;
142
143         tmp = cite (sent_date, from);
144         retval = g_strdup_printf ("%s%s\n", tmp, text);
145         g_free (tmp);
146
147         return retval;
148 }
149
150 gchar * 
151 modest_text_utils_inline (const gchar *text,
152                           const gchar *content_type,
153                           const gchar *from,
154                           time_t sent_date,
155                           const gchar *to,
156                           const gchar *subject)
157 {
158         gchar sent_str[101];
159         const gchar *plain_format = "%s\n%s %s\n%s %s\n%s %s\n%s %s\n\n%s";
160         const gchar *html_format = \
161                 "%s<br>\n<table width=\"100%\" border=\"0\" cellspacing=\"2\" cellpadding=\"2\">\n" \
162                 "<tr><td>%s</td><td>%s</td></tr>\n" \
163                 "<tr><td>%s</td><td>%s</td></tr>\n" \
164                 "<tr><td>%s</td><td>%s</td></tr>\n" \
165                 "<tr><td>%s</td><td>%s</td></tr>\n" \
166                 "<br><br>%s";
167         const gchar *format;
168
169         modest_text_utils_strftime (sent_str, 100, "%c", localtime (&sent_date));
170
171         if (!strcmp (content_type, "text/html"))
172                 /* TODO: extract the <body> of the HTML and pass it to
173                    the function */
174                 format = html_format;
175         else
176                 format = plain_format;
177
178         return g_strdup_printf (format, 
179                                 FORWARD_STRING,
180                                 FROM_STRING, from,
181                                 SENT_STRING, sent_str,
182                                 TO_STRING, to,
183                                 SUBJECT_STRING, subject,
184                                 text);
185 }
186
187 /* just to prevent warnings:
188  * warning: `%x' yields only last 2 digits of year in some locales
189  */
190 size_t
191 modest_text_utils_strftime(char *s, size_t max, const char  *fmt, const  struct tm *tm)
192 {
193         return strftime(s, max, fmt, tm);
194 }
195
196 gchar *
197 modest_text_utils_derived_subject (const gchar *subject, const gchar *prefix)
198 {
199         gchar *tmp;
200
201         g_return_val_if_fail (prefix, NULL);
202         
203         if (!subject)
204                 return g_strdup (prefix);
205
206         tmp = g_strchug (g_strdup (subject));
207
208         if (!strncmp (tmp, prefix, strlen (prefix))) {
209                 return tmp;
210         } else {
211                 g_free (tmp);
212                 return g_strdup_printf ("%s %s", prefix, subject);
213         }
214 }
215
216 gchar *
217 modest_text_utils_remove_address (const gchar *address_list, const gchar *address)
218 {
219         char *dup, *token, *ptr, *result;
220         GString *filtered_emails;
221
222         if (!address_list)
223                 return NULL;
224
225         /* Search for substring */
226         if (!strstr ((const char *) address_list, (const char *) address))
227                 return g_strdup (address_list);
228
229         dup = g_strdup (address_list);
230         filtered_emails = g_string_new (NULL);
231         
232         token = strtok_r (dup, ",", &ptr);
233
234         while (token != NULL) {
235                 /* Add to list if not found */
236                 if (!strstr ((const char *) token, (const char *) address)) {
237                         if (filtered_emails->len == 0)
238                                 g_string_append_printf (filtered_emails, "%s", token);
239                         else
240                                 g_string_append_printf (filtered_emails, ",%s", token);
241                 }
242                 token = strtok_r (NULL, ",", &ptr);
243         }
244         result = filtered_emails->str;
245
246         /* Clean */
247         g_free (dup);
248         g_string_free (filtered_emails, FALSE);
249
250         return result;
251 }
252
253 gchar*
254 modest_text_utils_convert_to_html (const gchar *data)
255 {
256         guint            i;
257         gboolean         first_space = TRUE;
258         GString         *html;      
259         gsize           len;
260
261         if (!data)
262                 return NULL;
263
264         len = strlen (data);
265         html = g_string_sized_new (len + 100);  /* just a  guess... */
266         
267         g_string_append_printf (html,
268                                 "<html>"
269                                 "<head>"
270                                 "<meta http-equiv=\"content-type\""
271                                 " content=\"text/html; charset=utf8\">"
272                                 "</head>"
273                                 "<body><tt>");
274         
275         /* replace with special html chars where needed*/
276         for (i = 0; i != len; ++i)  {
277                 char    kar = data[i]; 
278                 switch (kar) {
279                         
280                 case 0:  break; /* ignore embedded \0s */       
281                 case '<' : g_string_append   (html, "&lt;"); break;
282                 case '>' : g_string_append   (html, "&gt;"); break;
283                 case '&' : g_string_append   (html, "&quot;"); break;
284                 case '\n': g_string_append   (html, "<br>\n"); break;
285                 default:
286                         if (kar == ' ') {
287                                 g_string_append (html, first_space ? " " : "&nbsp;");
288                                 first_space = FALSE;
289                         } else  if (kar == '\t')
290                                 g_string_append (html, "&nbsp; &nbsp;&nbsp;");
291                         else {
292                                 int charnum = 0;
293                                 first_space = TRUE;
294                                 /* optimization trick: accumulate 'normal' chars, then copy */
295                                 do {
296                                         kar = data [++charnum + i];
297                                         
298                                 } while ((i + charnum < len) &&
299                                          (kar > '>' || (kar != '<' && kar != '>'
300                                                         && kar != '&' && kar !=  ' '
301                                                         && kar != '\n' && kar != '\t')));
302                                 g_string_append_len (html, &data[i], charnum);
303                                 i += (charnum  - 1);
304                         }
305                 }
306         }
307         
308         g_string_append (html, "</tt></body></html>");
309         hyperlinkify_plain_text (html);
310
311         return g_string_free (html, FALSE);
312 }
313
314 /* ******************************************************************* */
315 /* ************************* UTILIY FUNCTIONS ************************ */
316 /* ******************************************************************* */
317
318 static GString *
319 get_next_line (const gchar * b, const gsize blen, const gchar * iter)
320 {
321         GString *gs;
322         const gchar *i0;
323         
324         if (iter > b + blen)
325                 return g_string_new("");
326         
327         i0 = iter;
328         while (iter[0]) {
329                 if (iter[0] == '\n')
330                         break;
331                 iter++;
332         }
333         gs = g_string_new_len (i0, iter - i0);
334         return gs;
335 }
336 static int
337 get_indent_level (const char *l)
338 {
339         int indent = 0;
340
341         while (l[0]) {
342                 if (l[0] == '>') {
343                         indent++;
344                         if (l[1] == ' ') {
345                                 l++;
346                         }
347                 } else {
348                         break;
349                 }
350                 l++;
351
352         }
353
354         /*      if we hit the signature marker "-- ", we return -(indent + 1). This
355          *      stops reformatting.
356          */
357         if (strcmp (l, "-- ") == 0) {
358                 return -1 - indent;
359         } else {
360                 return indent;
361         }
362 }
363
364 static void
365 unquote_line (GString * l)
366 {
367         gchar *p;
368
369         p = l->str;
370         while (p[0]) {
371                 if (p[0] == '>') {
372                         if (p[1] == ' ') {
373                                 p++;
374                         }
375                 } else {
376                         break;
377                 }
378                 p++;
379         }
380         g_string_erase (l, 0, p - l->str);
381 }
382
383 static void
384 append_quoted (GString * buf, int indent, const GString * str,
385                const int cutpoint)
386 {
387         int i;
388
389         indent = indent < 0 ? abs (indent) - 1 : indent;
390         for (i = 0; i <= indent; i++) {
391                 g_string_append (buf, "> ");
392         }
393         if (cutpoint > 0) {
394                 g_string_append_len (buf, str->str, cutpoint);
395         } else {
396                 g_string_append (buf, str->str);
397         }
398         g_string_append (buf, "\n");
399 }
400
401 static int
402 get_breakpoint_utf8 (const gchar * s, gint indent, const gint limit)
403 {
404         gint index = 0;
405         const gchar *pos, *last;
406         gunichar *uni;
407
408         indent = indent < 0 ? abs (indent) - 1 : indent;
409
410         last = NULL;
411         pos = s;
412         uni = g_utf8_to_ucs4_fast (s, -1, NULL);
413         while (pos[0]) {
414                 if ((index + 2 * indent > limit) && last) {
415                         g_free (uni);
416                         return last - s;
417                 }
418                 if (g_unichar_isspace (uni[index])) {
419                         last = pos;
420                 }
421                 pos = g_utf8_next_char (pos);
422                 index++;
423         }
424         g_free (uni);
425         return strlen (s);
426 }
427
428 static int
429 get_breakpoint_ascii (const gchar * s, const gint indent, const gint limit)
430 {
431         gint i, last;
432
433         last = strlen (s);
434         if (last + 2 * indent < limit)
435                 return last;
436
437         for (i = strlen (s); i > 0; i--) {
438                 if (s[i] == ' ') {
439                         if (i + 2 * indent <= limit) {
440                                 return i;
441                         } else {
442                                 last = i;
443                         }
444                 }
445         }
446         return last;
447 }
448
449 static int
450 get_breakpoint (const gchar * s, const gint indent, const gint limit)
451 {
452
453         if (g_utf8_validate (s, -1, NULL)) {
454                 return get_breakpoint_utf8 (s, indent, limit);
455         } else {                /* assume ASCII */
456                 //g_warning("invalid UTF-8 in msg");
457                 return get_breakpoint_ascii (s, indent, limit);
458         }
459 }
460
461 static gchar *
462 cite (const time_t sent_date, const gchar *from)
463 {
464         gchar sent_str[101];
465
466         /* format sent_date */
467         modest_text_utils_strftime (sent_str, 100, "%c", localtime (&sent_date));
468         return g_strdup_printf (N_("On %s, %s wrote:\n"), sent_str, from);
469 }
470
471
472 static gchar *
473 modest_text_utils_quote_plain_text (const gchar *text, 
474                                     const gchar *cite, 
475                                     int limit)
476 {
477         const gchar *iter;
478         gint indent, breakpoint, rem_indent = 0;
479         GString *q, *l, *remaining;
480         gsize len;
481         gchar *tmp;
482
483         /* remaining will store the rest of the line if we have to break it */
484         q = g_string_new (cite);
485         remaining = g_string_new ("");
486
487         iter = text;
488         len = strlen(text);
489         do {
490                 l = get_next_line (text, len, iter);
491                 iter = iter + l->len + 1;
492                 indent = get_indent_level (l->str);
493                 unquote_line (l);
494
495                 if (remaining->len) {
496                         if (l->len && indent == rem_indent) {
497                                 g_string_prepend (l, " ");
498                                 g_string_prepend (l, remaining->str);
499                         } else {
500                                 do {
501                                         breakpoint =
502                                                 get_breakpoint (remaining->     str,
503                                                                 rem_indent,
504                                                                 limit);
505                                         append_quoted (q, rem_indent,
506                                                        remaining, breakpoint);
507                                         g_string_erase (remaining, 0,
508                                                         breakpoint);
509                                         if (remaining->str[0] == ' ') {
510                                                 g_string_erase (remaining, 0,
511                                                                 1);
512                                         }
513                                 } while (remaining->len);
514                         }
515                 }
516                 g_string_free (remaining, TRUE);
517                 breakpoint = get_breakpoint (l->str, indent, limit);
518                 remaining = g_string_new (l->str + breakpoint);
519                 if (remaining->str[0] == ' ') {
520                         g_string_erase (remaining, 0, 1);
521                 }
522                 rem_indent = indent;
523                 append_quoted (q, indent, l, breakpoint);
524                 g_string_free (l, TRUE);
525         } while ((iter < text + len) || (remaining->str[0]));
526
527         return g_string_free (q, FALSE);
528 }
529
530 static gchar*
531 modest_text_utils_quote_html (const gchar *text, 
532                               const gchar *cite, 
533                               int limit)
534 {
535         const gchar *format = \
536                 "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n" \
537                 "<html>\n" \
538                 "<body>\n" \
539                 "%s" \
540                 "<blockquote type=\"cite\">\n%s\n</blockquote>\n" \
541                 "</body>\n" \
542                 "</html>\n";
543
544         return g_strdup_printf (format, cite, text);
545 }
546
547 static gint 
548 cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2)
549 {
550         return match2->offset - match1->offset;
551 }
552
553
554
555 /*
556  * check if the match is inside an existing match... */
557 static void
558 chk_partial_match (const url_match_t *match, guint* offset)
559 {
560         if (*offset >= match->offset && *offset < match->offset + match->len)
561                 *offset = -1;
562 }
563
564 static GSList*
565 get_url_matches (GString *txt)
566 {
567         regmatch_t rm;
568         guint rv, i, offset = 0;
569         GSList *match_list = NULL;
570
571         static url_match_pattern_t patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS;
572         const size_t pattern_num = sizeof(patterns)/sizeof(url_match_pattern_t);
573
574         /* initalize the regexps */
575         for (i = 0; i != pattern_num; ++i) {
576                 patterns[i].preg = g_new0 (regex_t,1);
577                 g_assert(regcomp (patterns[i].preg, patterns[i].regex,
578                                   REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0);
579         }
580         /* find all the matches */
581         for (i = 0; i != pattern_num; ++i) {
582                 offset     = 0; 
583                 while (1) {
584                         int test_offset;
585                         if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) {
586                                 g_assert (rv == REG_NOMATCH); /* this should not happen */
587                                 break; /* try next regexp */ 
588                         }
589                         if (rm.rm_so == -1)
590                                 break;
591
592                         /* FIXME: optimize this */
593                         /* to avoid partial matches on something that was already found... */
594                         /* check_partial_match will put -1 in the data ptr if that is the case */
595                         test_offset = offset + rm.rm_so;
596                         g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset);
597                         
598                         /* make a list of our matches (<offset, len, prefix> tupels)*/
599                         if (test_offset != -1) {
600                                 url_match_t *match = g_new (url_match_t,1);
601                                 match->offset = offset + rm.rm_so;
602                                 match->len    = rm.rm_eo - rm.rm_so;
603                                 match->prefix = patterns[i].prefix;
604                                 match_list = g_slist_prepend (match_list, match);
605                         }
606                         offset += rm.rm_eo;
607                 }
608         }
609
610         for (i = 0; i != pattern_num; ++i) {
611                 regfree (patterns[i].preg);
612                 g_free  (patterns[i].preg);
613         } /* don't free patterns itself -- it's static */
614         
615         /* now sort the list, so the matches are in reverse order of occurence.
616          * that way, we can do the replacements starting from the end, so we don't need
617          * to recalculate the offsets
618          */
619         match_list = g_slist_sort (match_list,
620                                    (GCompareFunc)cmp_offsets_reverse); 
621         return match_list;      
622 }
623
624
625
626 static void
627 hyperlinkify_plain_text (GString *txt)
628 {
629         GSList *cursor;
630         GSList *match_list = get_url_matches (txt);
631
632         /* we will work backwards, so the offsets stay valid */
633         for (cursor = match_list; cursor; cursor = cursor->next) {
634
635                 url_match_t *match = (url_match_t*) cursor->data;
636                 gchar *url  = g_strndup (txt->str + match->offset, match->len);
637                 gchar *repl = NULL; /* replacement  */
638
639                 /* the prefix is NULL: use the one that is already there */
640                 repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
641                                         match->prefix ? match->prefix : "", url, url);
642
643                 /* replace the old thing with our hyperlink
644                  * replacement thing */
645                 g_string_erase  (txt, match->offset, match->len);
646                 g_string_insert (txt, match->offset, repl);
647                 
648                 g_free (url);
649                 g_free (repl);
650
651                 g_free (cursor->data);  
652         }
653         
654         g_slist_free (match_list);
655 }
656
657
658
659 gchar*
660 modest_text_utils_display_address (gchar *address)
661 {
662         gchar *cursor;
663         
664         if (!address)
665                 return NULL;
666
667         g_return_val_if_fail (g_utf8_validate (address, -1, NULL), NULL);
668
669         g_strchug (address); /* remove leading whitespace */
670
671         /*  <email@address> from display name */
672         cursor = g_strstr_len (address, strlen(address), "<");
673         if (cursor == address) /* there's nothing else? leave it */
674                 return address;
675         if (cursor) 
676                 cursor[0]='\0';
677
678         /* remove (bla bla) from display name */
679         cursor = g_strstr_len (address, strlen(address), "(");
680         if (cursor == address) /* there's nothing else? leave it */
681                 return address;
682         if (cursor) 
683                 cursor[0]='\0';
684
685         g_strchomp (address); /* remove trailing whitespace */
686
687         return address;
688 }
689