1 /**
2  * @file
3  * Handling of international domain names
4  *
5  * @authors
6  * Copyright (C) 2003,2005,2008-2009 Thomas Roessler <roessler@does-not-exist.org>
7  *
8  * @copyright
9  * This program is free software: you can redistribute it and/or modify it under
10  * the terms of the GNU General Public License as published by the Free Software
11  * Foundation, either version 2 of the License, or (at your option) any later
12  * version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17  * details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program.  If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 /**
24  * @page addr_idna International domain names
25  *
26  * Handling of international domain names
27  */
28 
29 #include "config.h"
30 #include <stdint.h>
31 #include <stdio.h>
32 #include "mutt/lib.h"
33 #include "config/lib.h"
34 #include "core/lib.h"
35 #include "idna2.h"
36 #ifdef HAVE_LIBIDN
37 #include <stdbool.h>
38 #include <string.h>
39 #endif
40 #ifdef HAVE_STRINGPREP_H
41 #include <stringprep.h>
42 #elif defined(HAVE_IDN_STRINGPREP_H)
43 #include <idn/stringprep.h>
44 #endif
45 #define IDN2_SKIP_LIBIDN_COMPAT
46 #ifdef HAVE_IDN2_H
47 #include <idn2.h>
48 #elif defined(HAVE_IDN_IDN2_H)
49 #include <idn/idn2.h>
50 #elif defined(HAVE_IDNA_H)
51 #include <idna.h>
52 #elif defined(HAVE_IDN_IDNA_H)
53 #include <idn/idna.h>
54 #endif
55 
56 #if defined(HAVE_IDN2_H) || defined(HAVE_IDN_IDN2_H)
57 #define IDN_VERSION 2
58 #elif defined(HAVE_IDNA_H) || defined(HAVE_IDN_IDNA_H)
59 #define IDN_VERSION 1
60 #endif
61 
62 #ifdef HAVE_LIBIDN
63 /* Work around incompatibilities in the libidn API */
64 #if (!defined(HAVE_IDNA_TO_ASCII_8Z) && defined(HAVE_IDNA_TO_ASCII_FROM_UTF8))
65 #define idna_to_ascii_8z(input, output, flags)                                 \
66   idna_to_ascii_from_utf8(input, output, (flags) &1, ((flags) &2) ? 1 : 0)
67 #endif
68 #if (!defined(HAVE_IDNA_TO_ASCII_LZ) && defined(HAVE_IDNA_TO_ASCII_FROM_LOCALE))
69 #define idna_to_ascii_lz(input, output, flags)                                 \
70   idna_to_ascii_from_locale(input, output, (flags) &1, ((flags) &2) ? 1 : 0)
71 #endif
72 #if (!defined(HAVE_IDNA_TO_UNICODE_8Z8Z) && defined(HAVE_IDNA_TO_UNICODE_UTF8_FROM_UTF8))
73 #define idna_to_unicode_8z8z(input, output, flags)                             \
74   idna_to_unicode_utf8_from_utf8(input, output, (flags) &1, ((flags) &2) ? 1 : 0)
75 #endif
76 #endif /* HAVE_LIBIDN */
77 
78 #ifdef HAVE_LIBIDN
79 /**
80  * check_idn - Is domain in Punycode?
81  * @param domain Domain to test
82  * @retval true At least one part of domain is in Punycode
83  */
check_idn(char * domain)84 static bool check_idn(char *domain)
85 {
86   if (!domain)
87     return false;
88 
89   if (mutt_istr_startswith(domain, "xn--"))
90     return true;
91 
92   while ((domain = strchr(domain, '.')))
93   {
94     if (mutt_istr_startswith(++domain, "xn--"))
95       return true;
96   }
97 
98   return false;
99 }
100 
101 /**
102  * mutt_idna_to_ascii_lz - Convert a domain to Punycode
103  * @param[in]  input  Domain
104  * @param[out] output Result
105  * @param[in]  flags  Flags, e.g. IDNA_ALLOW_UNASSIGNED
106  * @retval 0 Success
107  * @retval >0 Failure, error code
108  *
109  * Convert a domain from the current locale to Punycode.
110  *
111  * @note The caller must free output
112  */
mutt_idna_to_ascii_lz(const char * input,char ** output,uint8_t flags)113 int mutt_idna_to_ascii_lz(const char *input, char **output, uint8_t flags)
114 {
115   if (!input || !output)
116     return 1;
117 
118 #if (IDN_VERSION == 2)
119   return idn2_to_ascii_8z(input, output, flags | IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL);
120 #else
121   return idna_to_ascii_lz(input, output, flags);
122 #endif
123 }
124 #endif /* HAVE_LIBIDN */
125 
126 /**
127  * mutt_idna_intl_to_local - Convert an email's domain from Punycode
128  * @param user   Username
129  * @param domain Domain
130  * @param flags  Flags, e.g. #MI_MAY_BE_IRREVERSIBLE
131  * @retval ptr  Newly allocated local email address
132  * @retval NULL Error in conversion
133  *
134  * If `$idn_decode` is set, then the domain will be converted from Punycode.
135  * For example, "xn--ls8h.la" becomes the emoji domain: ":poop:.la"
136  * Then the user and domain are changed from 'utf-8' to the encoding in
137  * `$charset`.
138  *
139  * If the flag #MI_MAY_BE_IRREVERSIBLE is NOT given, then the results will be
140  * checked to make sure that the transformation is "undo-able".
141  *
142  * @note The caller must free the returned string.
143  */
mutt_idna_intl_to_local(const char * user,const char * domain,uint8_t flags)144 char *mutt_idna_intl_to_local(const char *user, const char *domain, uint8_t flags)
145 {
146   char *mailbox = NULL;
147   char *reversed_user = NULL, *reversed_domain = NULL;
148   char *tmp = NULL;
149 
150   char *local_user = mutt_str_dup(user);
151   char *local_domain = mutt_str_dup(domain);
152 
153 #ifdef HAVE_LIBIDN
154   bool is_idn_encoded = check_idn(local_domain);
155   const bool c_idn_decode = cs_subset_bool(NeoMutt->sub, "idn_decode");
156   if (is_idn_encoded && c_idn_decode)
157   {
158 #if (IDN_VERSION == 2)
159     if (idn2_to_unicode_8z8z(local_domain, &tmp, IDN2_ALLOW_UNASSIGNED) != IDN2_OK)
160 #else
161     if (idna_to_unicode_8z8z(local_domain, &tmp, IDNA_ALLOW_UNASSIGNED) != IDNA_SUCCESS)
162 #endif
163     {
164       goto cleanup;
165     }
166     mutt_str_replace(&local_domain, tmp);
167     FREE(&tmp);
168   }
169 #endif /* HAVE_LIBIDN */
170 
171   /* we don't want charset-hook effects, so we set flags to 0 */
172   const char *const c_charset = cs_subset_string(NeoMutt->sub, "charset");
173   if (mutt_ch_convert_string(&local_user, "utf-8", c_charset, MUTT_ICONV_NO_FLAGS) != 0)
174     goto cleanup;
175 
176   if (mutt_ch_convert_string(&local_domain, "utf-8", c_charset, MUTT_ICONV_NO_FLAGS) != 0)
177     goto cleanup;
178 
179   /* make sure that we can convert back and come out with the same
180    * user and domain name.  */
181   if ((flags & MI_MAY_BE_IRREVERSIBLE) == 0)
182   {
183     reversed_user = mutt_str_dup(local_user);
184 
185     if (mutt_ch_convert_string(&reversed_user, c_charset, "utf-8", MUTT_ICONV_NO_FLAGS) != 0)
186     {
187       mutt_debug(LL_DEBUG1, "Not reversible. Charset conv to utf-8 failed for user = '%s'\n",
188                  reversed_user);
189       goto cleanup;
190     }
191 
192     if (!mutt_istr_equal(user, reversed_user))
193     {
194       mutt_debug(LL_DEBUG1, "#1 Not reversible. orig = '%s', reversed = '%s'\n",
195                  user, reversed_user);
196       goto cleanup;
197     }
198 
199     reversed_domain = mutt_str_dup(local_domain);
200 
201     if (mutt_ch_convert_string(&reversed_domain, c_charset, "utf-8", MUTT_ICONV_NO_FLAGS) != 0)
202     {
203       mutt_debug(LL_DEBUG1, "Not reversible. Charset conv to utf-8 failed for domain = '%s'\n",
204                  reversed_domain);
205       goto cleanup;
206     }
207 
208 #ifdef HAVE_LIBIDN
209     /* If the original domain was UTF-8, idna encoding here could
210      * produce a non-matching domain!  Thus we only want to do the
211      * idna_to_ascii_8z() if the original domain was IDNA encoded.  */
212     if (is_idn_encoded && c_idn_decode)
213     {
214 #if (IDN_VERSION == 2)
215       if (idn2_to_ascii_8z(reversed_domain, &tmp,
216                            IDN2_ALLOW_UNASSIGNED | IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL) != IDN2_OK)
217 #else
218       if (idna_to_ascii_8z(reversed_domain, &tmp, IDNA_ALLOW_UNASSIGNED) != IDNA_SUCCESS)
219 #endif
220       {
221         mutt_debug(LL_DEBUG1, "Not reversible. idna_to_ascii_8z failed for domain = '%s'\n",
222                    reversed_domain);
223         goto cleanup;
224       }
225       mutt_str_replace(&reversed_domain, tmp);
226     }
227 #endif /* HAVE_LIBIDN */
228 
229     if (!mutt_istr_equal(domain, reversed_domain))
230     {
231       mutt_debug(LL_DEBUG1, "#2 Not reversible. orig = '%s', reversed = '%s'\n",
232                  domain, reversed_domain);
233       goto cleanup;
234     }
235   }
236 
237   mailbox = mutt_mem_malloc(mutt_str_len(local_user) + mutt_str_len(local_domain) + 2);
238   sprintf(mailbox, "%s@%s", NONULL(local_user), NONULL(local_domain));
239 
240 cleanup:
241   FREE(&local_user);
242   FREE(&local_domain);
243   FREE(&tmp);
244   FREE(&reversed_domain);
245   FREE(&reversed_user);
246 
247   return mailbox;
248 }
249 
250 /**
251  * mutt_idna_local_to_intl - Convert an email's domain to Punycode
252  * @param user   Username
253  * @param domain Domain
254  * @retval ptr  Newly allocated Punycode email address
255  * @retval NULL Error in conversion
256  *
257  * The user and domain are assumed to be encoded according to `$charset`.
258  * They are converted to 'utf-8'.  If `$idn_encode` is set, then the domain
259  * will be converted to Punycode.  For example, the emoji domain:
260  * ":poop:.la" becomes "xn--ls8h.la"
261  *
262  * @note The caller must free the returned string.
263  */
mutt_idna_local_to_intl(const char * user,const char * domain)264 char *mutt_idna_local_to_intl(const char *user, const char *domain)
265 {
266   char *mailbox = NULL;
267   char *tmp = NULL;
268 
269   char *intl_user = mutt_str_dup(user);
270   char *intl_domain = mutt_str_dup(domain);
271 
272   /* we don't want charset-hook effects, so we set flags to 0 */
273   const char *const c_charset = cs_subset_string(NeoMutt->sub, "charset");
274   if (mutt_ch_convert_string(&intl_user, c_charset, "utf-8", MUTT_ICONV_NO_FLAGS) != 0)
275     goto cleanup;
276 
277   if (mutt_ch_convert_string(&intl_domain, c_charset, "utf-8", MUTT_ICONV_NO_FLAGS) != 0)
278     goto cleanup;
279 
280 #ifdef HAVE_LIBIDN
281   const bool c_idn_encode = cs_subset_bool(NeoMutt->sub, "idn_encode");
282   if (c_idn_encode)
283   {
284 #if (IDN_VERSION == 2)
285     if (idn2_to_ascii_8z(intl_domain, &tmp,
286                          IDN2_ALLOW_UNASSIGNED | IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL) != IDN2_OK)
287 #else
288     if (idna_to_ascii_8z(intl_domain, &tmp, IDNA_ALLOW_UNASSIGNED) != IDNA_SUCCESS)
289 #endif
290     {
291       goto cleanup;
292     }
293     mutt_str_replace(&intl_domain, tmp);
294   }
295 #endif /* HAVE_LIBIDN */
296 
297   mailbox = mutt_mem_malloc(mutt_str_len(intl_user) + mutt_str_len(intl_domain) + 2);
298   sprintf(mailbox, "%s@%s", NONULL(intl_user), NONULL(intl_domain));
299 
300 cleanup:
301   FREE(&intl_user);
302   FREE(&intl_domain);
303   FREE(&tmp);
304 
305   return mailbox;
306 }
307 
308 /**
309  * mutt_idna_print_version - Create an IDN version string
310  * @retval ptr Version string
311  *
312  * @note This is a static string and must not be freed.
313  */
mutt_idna_print_version(void)314 const char *mutt_idna_print_version(void)
315 {
316   static char vstring[256];
317 
318 #ifdef HAVE_LIBIDN
319 #if (IDN_VERSION == 2)
320   snprintf(vstring, sizeof(vstring), "libidn2: %s (compiled with %s)",
321            idn2_check_version(NULL), IDN2_VERSION);
322 #elif (IDN_VERSION == 1)
323   snprintf(vstring, sizeof(vstring), "libidn: %s (compiled with %s)",
324            stringprep_check_version(NULL), STRINGPREP_VERSION);
325 #endif
326 #endif /* HAVE_LIBIDN */
327 
328   return vstring;
329 }
330