* fix the url handling of '&'; it's a bit hairy because of the
[modest] / src / modest-text-utils.c
index ce015be..3a89582 100644 (file)
  */
 #define HYPERLINKIFY_MAX_LENGTH (1024*50)
 
  */
 #define HYPERLINKIFY_MAX_LENGTH (1024*50)
 
+
+/*
+ * we mark the ampersand with \007 when converting text->html
+ * because after text->html we do hyperlink detecting, which
+ * could be screwed up by the ampersand
+ */
+#define MARK_AMP '\007'
+#define MARK_AMP_STR "\007"
+
 /*
  * we need these regexps to find URLs in plain text e-mails
  */
 /*
  * we need these regexps to find URLs in plain text e-mails
  */
@@ -81,18 +90,19 @@ struct _url_match_t {
        const gchar* prefix;
 };
 
        const gchar* prefix;
 };
 
+/* note: match MARK_AMP_STR as well, because after txt->html, a '&' will look like $(MARK_AMP_STR)"amp;" */
 #define MAIL_VIEWER_URL_MATCH_PATTERNS  {                              \
 #define MAIL_VIEWER_URL_MATCH_PATTERNS  {                              \
-       { "(file|rtsp|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\
+       { "^(file|rtsp|http|ftp|https)://[-a-z0-9_$.+!*(),;:@%=?/~#" MARK_AMP_STR "]+[-a-z0-9_$%" MARK_AMP_STR "=?/~#]",\
          NULL, NULL },\
          NULL, NULL },\
-       { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
+       { "^www\\.[-a-z0-9_$.+!*(),;:@%=?/~#" MARK_AMP_STR "]+[-a-z0-9_$%" MARK_AMP_STR "=?/~#]",\
                        NULL, "http://" },                              \
                        NULL, "http://" },                              \
-       { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
+       { "^ftp\\.[-a-z0-9_$.+!*(),;:@%=?/~#" MARK_AMP_STR "]+[-a-z0-9_$%" MARK_AMP_STR "=?/~#]",\
          NULL, "ftp://" },\
          NULL, "ftp://" },\
-       { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \
+       { "^(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.+]+", \
           NULL, NULL},                                             \
           NULL, NULL},                                             \
-       { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+",                    \
+       { "^mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+",                   \
          NULL, NULL},\
          NULL, NULL},\
-       { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
+       { "^[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
          NULL, "mailto:"}\
        }
 
          NULL, "mailto:"}\
        }
 
@@ -100,7 +110,7 @@ const gchar account_title_forbidden_chars[] = {
        '\\', '/', ':', '*', '?', '\'', '<', '>', '|', '^'
 };
 const gchar folder_name_forbidden_chars[] = {
        '\\', '/', ':', '*', '?', '\'', '<', '>', '|', '^'
 };
 const gchar folder_name_forbidden_chars[] = {
-       '<', '>', ':', '\'', '/', '\\', '|', '?', '*', '^', '%', '$'
+       '<', '>', ':', '\'', '/', '\\', '|', '?', '*', '^', '%', '$', '#', '&'
 };
 const gchar user_name_forbidden_chars[] = {
        '<', '>'
 };
 const gchar user_name_forbidden_chars[] = {
        '<', '>'
@@ -278,7 +288,7 @@ modest_text_utils_derived_subject (const gchar *subject, const gchar *prefix)
 gchar*
 modest_text_utils_remove_address (const gchar *address_list, const gchar *address)
 {
 gchar*
 modest_text_utils_remove_address (const gchar *address_list, const gchar *address)
 {
-       gchar *dup, *token, *ptr, *result;
+       gchar *dup, *token, *ptr = NULL, *result;
        GString *filtered_emails;
        gchar *email_address;
 
        GString *filtered_emails;
        gchar *email_address;
 
@@ -320,10 +330,48 @@ modest_text_utils_remove_address (const gchar *address_list, const gchar *addres
        return result;
 }
 
        return result;
 }
 
+
+gchar*
+modest_text_utils_remove_duplicate_addresses (const gchar *address_list)
+{
+       GSList *addresses, *cursor;
+       GHashTable *table;
+       gchar *new_list;
+       
+       g_return_val_if_fail (address_list, NULL);
+
+       table = g_hash_table_new (g_str_hash, g_str_equal);
+       addresses = modest_text_utils_split_addresses_list (address_list);
+
+       new_list = g_strdup("");
+       cursor = addresses;
+       while (cursor) {
+               const gchar* address = (const gchar*)cursor->data;
+
+               /* ignore the address if already seen */
+               if (g_hash_table_lookup (table, address) == 0) {
+               
+                       gchar *tmp = g_strjoin (",", new_list, address, NULL);
+                       g_free (new_list);
+                       new_list = tmp;
+                       
+                       g_hash_table_insert (table, (gchar*)address, GINT_TO_POINTER(1));
+               }
+               cursor = g_slist_next (cursor);
+       }
+
+       g_hash_table_destroy (table);
+       g_slist_foreach (addresses, (GFunc)g_free, NULL);
+       g_slist_free (addresses);
+
+       return new_list;
+}
+
+
 static void
 static void
-modest_text_utils_convert_buffer_to_html (GString *html, const gchar *data, gssize n)
+modest_text_utils_convert_buffer_to_html_start (GString *html, const gchar *data, gssize n)
 {
 {
-       guint            i;
+       guint           i;
        gboolean        space_seen = FALSE;
        guint           break_dist = 0; /* distance since last break point */
 
        gboolean        space_seen = FALSE;
        guint           break_dist = 0; /* distance since last break point */
 
@@ -349,16 +397,21 @@ modest_text_utils_convert_buffer_to_html (GString *html, const gchar *data, gssi
                }
                
                switch (kar) {
                }
                
                switch (kar) {
-               case 0:  break; /* ignore embedded \0s */       
-               case '<'  : g_string_append (html, "&lt;");   break;
-               case '>'  : g_string_append (html, "&gt;");   break;
-               case '&'  : g_string_append (html, "&amp;");  break;
-               case '"'  : g_string_append (html, "&quot;");  break;
+               case 0:
+               case MARK_AMP: /* this is a temp place holder for '&'; we can only
+                               * set the real '&' after hyperlink translation, otherwise
+                               * we might screw that up */
+                       break; /* ignore embedded \0s and MARK_AMP */   
+               case '<'  : g_string_append (html, MARK_AMP_STR "lt;");   break;
+               case '>'  : g_string_append (html, MARK_AMP_STR "gt;");   break;
+               case '&'  : g_string_append (html, MARK_AMP_STR "amp;");  break;
+               case '"'  : g_string_append (html, MARK_AMP_STR "quot;");  break;
 
                /* don't convert &apos; --> wpeditor will try to re-convert it... */    
                //case '\'' : g_string_append (html, "&apos;"); break;
 
                /* don't convert &apos; --> wpeditor will try to re-convert it... */    
                //case '\'' : g_string_append (html, "&apos;"); break;
-               case '\n' : g_string_append (html, "<br>\n");              break_dist= 0; break;
-               case '\t' : g_string_append (html, "&nbsp;&nbsp;&nbsp; "); break_dist=0; break; /* note the space at the end*/
+               case '\n' : g_string_append (html, "<br>\n");break_dist= 0; break;
+               case '\t' : g_string_append (html, MARK_AMP_STR "nbsp;" MARK_AMP_STR "nbsp;" MARK_AMP_STR "nbsp; ");
+                       break_dist=0; break; /* note the space at the end*/
                case ' ':
                        break_dist = 0;
                        if (space_seen) { /* second space in a row */
                case ' ':
                        break_dist = 0;
                        if (space_seen) { /* second space in a row */
@@ -373,6 +426,18 @@ modest_text_utils_convert_buffer_to_html (GString *html, const gchar *data, gssi
        }
 }
 
        }
 }
 
+
+static void
+modest_text_utils_convert_buffer_to_html_finish (GString *html)
+{
+       int i;
+       /* replace all our MARK_AMPs with real ones */
+       for (i = 0; i != html->len; ++i)
+               if ((html->str)[i] == MARK_AMP)
+                       (html->str)[i] = '&';
+}
+
+
 gchar*
 modest_text_utils_convert_to_html (const gchar *data)
 {
 gchar*
 modest_text_utils_convert_to_html (const gchar *data)
 {
@@ -393,13 +458,15 @@ modest_text_utils_convert_to_html (const gchar *data)
                                "</head>"
                                "<body>");
 
                                "</head>"
                                "<body>");
 
-       modest_text_utils_convert_buffer_to_html (html, data, -1);
+       modest_text_utils_convert_buffer_to_html_start (html, data, -1);
        
        g_string_append (html, "</body></html>");
 
        if (len <= HYPERLINKIFY_MAX_LENGTH)
                hyperlinkify_plain_text (html);
 
        
        g_string_append (html, "</body></html>");
 
        if (len <= HYPERLINKIFY_MAX_LENGTH)
                hyperlinkify_plain_text (html);
 
+       modest_text_utils_convert_buffer_to_html_finish (html);
+       
        return g_string_free (html, FALSE);
 }
 
        return g_string_free (html, FALSE);
 }
 
@@ -417,11 +484,13 @@ modest_text_utils_convert_to_html_body (const gchar *data, gssize n, gboolean hy
                n = strlen (data);
        html = g_string_sized_new (1.5 * n);    /* just a  guess... */
 
                n = strlen (data);
        html = g_string_sized_new (1.5 * n);    /* just a  guess... */
 
-       modest_text_utils_convert_buffer_to_html (html, data, n);
+       modest_text_utils_convert_buffer_to_html_start (html, data, n);
 
        if (hyperlinkify && (n < HYPERLINKIFY_MAX_LENGTH))
                hyperlinkify_plain_text (html);
 
 
        if (hyperlinkify && (n < HYPERLINKIFY_MAX_LENGTH))
                hyperlinkify_plain_text (html);
 
+       modest_text_utils_convert_buffer_to_html_finish (html);
+       
        return g_string_free (html, FALSE);
 }
 
        return g_string_free (html, FALSE);
 }
 
@@ -483,6 +552,7 @@ modest_text_utils_get_addresses_indexes (const gchar *addresses, GSList **start_
        return;
 }
 
        return;
 }
 
+#if 0
 GSList *
 modest_text_utils_split_addresses_list (const gchar *addresses)
 {
 GSList *
 modest_text_utils_split_addresses_list (const gchar *addresses)
 {
@@ -525,6 +595,43 @@ modest_text_utils_split_addresses_list (const gchar *addresses)
        return result;
 
 }
        return result;
 
 }
+#endif
+
+
+
+
+GSList *
+modest_text_utils_split_addresses_list (const gchar *addresses)
+{
+       GSList *head;
+       const gchar *my_addrs = addresses;
+       const gchar *end;
+       gchar *addr;
+       
+       /* skip any space, ',', ';' at the start */
+       while (my_addrs && (my_addrs[0] == ' ' || my_addrs[0] == ',' || my_addrs[0] == ';'))
+              ++my_addrs;
+
+       /* are we at the end of addresses list? */
+       if (!my_addrs[0])
+               return NULL;
+       
+       /* nope, we are at the start of some address
+        * now, let's find the end of the address */
+       end = my_addrs + 1;
+       while (end[0] && end[0] != ',' && end[0] != ';')
+               ++end;
+
+       /* we got the address; copy it and remove trailing whitespace */
+       addr = g_strndup (my_addrs, end - my_addrs);
+       g_strchomp (addr);
+
+       head = g_slist_append (NULL, addr);
+       head->next = modest_text_utils_split_addresses_list (end); /* recurse */
+
+       return head;
+}
+
 
 void
 modest_text_utils_address_range_at_position (const gchar *recipients_list,
 
 void
 modest_text_utils_address_range_at_position (const gchar *recipients_list,
@@ -866,8 +973,11 @@ compile_patterns ()
                patterns[i].preg = g_slice_new0 (regex_t);
                
                /* this should not happen */
                patterns[i].preg = g_slice_new0 (regex_t);
                
                /* this should not happen */
-               g_return_val_if_fail (regcomp (patterns[i].preg, patterns[i].regex,
-                                              REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0, FALSE);
+               if (regcomp (patterns[i].preg, patterns[i].regex,
+                            REG_ICASE|REG_EXTENDED|REG_NEWLINE) != 0) {
+                       g_warning ("%s: error in regexp:\n%s\n", __FUNCTION__, patterns[i].regex);
+                       return FALSE;
+               }
        }
        return TRUE;
 }
        }
        return TRUE;
 }
@@ -946,8 +1056,7 @@ get_url_matches (GString *txt)
                                match->len    = rm.rm_eo - rm.rm_so;
                                match->prefix = patterns[i].prefix;
                                match_list = g_slist_prepend (match_list, match);
                                match->len    = rm.rm_eo - rm.rm_so;
                                match->prefix = patterns[i].prefix;
                                match_list = g_slist_prepend (match_list, match);
-                       }
-                               
+                       }               
                        offset += rm.rm_eo;
                }
        }
                        offset += rm.rm_eo;
                }
        }
@@ -965,6 +1074,29 @@ get_url_matches (GString *txt)
 
 
 
 
 
 
+/* replace all occurences of needle in haystack with repl*/
+static gchar*
+replace_string (const gchar *haystack, const gchar *needle, gchar repl)
+{
+       gchar *str, *cursor;
+
+       if (!haystack || !needle || strlen(needle) == 0)
+               return haystack ? g_strdup(haystack) : NULL;
+       
+       str = g_strdup (haystack);
+
+       for (cursor = str; cursor && *cursor; ++cursor) {
+               if (g_str_has_prefix (cursor, needle)) {
+                       cursor[0] = repl;
+                       memmove (cursor + 1,
+                                cursor + strlen (needle),
+                                strlen (cursor + strlen (needle)) + 1);
+               }
+       }
+       
+       return str;
+}
+
 static void
 hyperlinkify_plain_text (GString *txt)
 {
 static void
 hyperlinkify_plain_text (GString *txt)
 {
@@ -978,10 +1110,17 @@ hyperlinkify_plain_text (GString *txt)
                gchar *url  = g_strndup (txt->str + match->offset, match->len);
                gchar *repl = NULL; /* replacement  */
 
                gchar *url  = g_strndup (txt->str + match->offset, match->len);
                gchar *repl = NULL; /* replacement  */
 
+               /* the string still contains $(MARK_AMP_STR)"amp;" for each
+                * '&' in the original, because of the text->html conversion.
+                * in the href-URL (and only there), we must convert that back to
+                * '&'
+                */
+               gchar *href_url = replace_string (url, MARK_AMP_STR "amp;", '&');
+               
                /* the prefix is NULL: use the one that is already there */
                repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
                                        match->prefix ? match->prefix : EMPTY_STRING, 
                /* the prefix is NULL: use the one that is already there */
                repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
                                        match->prefix ? match->prefix : EMPTY_STRING, 
-                                       url, url);
+                                       href_url, url);
 
                /* replace the old thing with our hyperlink
                 * replacement thing */
 
                /* replace the old thing with our hyperlink
                 * replacement thing */
@@ -990,6 +1129,7 @@ hyperlinkify_plain_text (GString *txt)
                
                g_free (url);
                g_free (repl);
                
                g_free (url);
                g_free (repl);
+               g_free (href_url);
 
                g_slice_free (url_match_t, match);      
        }
 
                g_slice_free (url_match_t, match);      
        }
@@ -1218,7 +1358,7 @@ modest_text_utils_validate_domain_name (const gchar *domain)
 {
        gboolean valid = FALSE;
        regex_t rx;
 {
        gboolean valid = FALSE;
        regex_t rx;
-       const gchar* domain_regex = "^[a-z0-9]([.]?[a-z0-9-])*[a-z0-9]$";
+       const gchar* domain_regex = "^([a-z0-9-]*[a-z0-9]\\.)+[a-z0-9-]*[a-z0-9]$";
 
        g_return_val_if_fail (domain, FALSE);
        
 
        g_return_val_if_fail (domain, FALSE);