I18N: new ${imapfolder_<sep>:<string>} expansion item. Bug 420
authorJeremy Harris <jgh146exb@wizmail.org>
Mon, 4 May 2015 16:02:27 +0000 (17:02 +0100)
committerJeremy Harris <jgh146exb@wizmail.org>
Mon, 4 May 2015 22:46:20 +0000 (23:46 +0100)
doc/doc-txt/NewStuff
doc/doc-txt/experimental-spec.txt
src/OS/Makefile-Base
src/scripts/MakeLinks
src/src/expand.c
src/src/functions.h
src/src/imap_utf7.c [new file with mode: 0644]
test/confs/4200
test/scripts/4200-International/4200
test/stdout/4200

index ef6a6dea97da954843f9062562034e66752c1ccb..cac6f7c104426e44ada79ed93bcc03c4692b122f 100644 (file)
@@ -28,6 +28,9 @@ Version 4.86
  8. If built with EXPERIMENTAL_INTERNATIONAL, support is included for
     the transmission of UTF-8 envelope addresses.
 
+ 9. If built with EXPERIMENTAL_INTERNATIONAL, an expansion item for a commonly
+    used encoding of Maildir folder names.
+
 
 Version 4.85
 ------------
index d446f6b67716c19932c4e29cf4f85d6f5f48d231..64916e4a9e3403351816a5c25488c2339014b8a6 100644 (file)
@@ -1271,8 +1271,9 @@ $tls_out_tlsa_usage (detailed above).
 
 
 
-SMTPUTF8
+INTERNATIONAL
 ------------------------------------------------------------
+SMTPUTF8
 Internationalised mail name handling.
 RFCs 6530, 6533, 5890
 
@@ -1337,6 +1338,36 @@ Known issues:
  - DSN unitext handling is not present
  - no provision for converting logging from or to UTF-8
 
+----
+IMAP folder names
+
+New expansion operator:
+
+${imapfolder {<string>} {<sep>} {<specials>}}
+
+The string is converted from the charset specified by the headers charset 
+command (in a filter file) or headers_charset global option, to the
+modified UTF-7 encoding specified by RFC 2060, with the following
+exception: All occurences of <sep> (which has to be a single character)
+are replaced with periods ("."), and all periods and slashes that aren't
+<sep> and are not in the <specials> string are BASE64 encoded.
+
+The third argument can be omitted, defaulting to an empty string.
+The second argument can be omitted, defaulting to "/".
+
+This is the encoding used by Courier for Maildir names on disk, and followed
+by many other IMAP servers.
+
+   Example 1: ${imapfolder {Foo/Bar}}       yields "Foo.Bar".
+   Example 2: ${imapfolder {Foo/Bar}{.}{/}} yields "Foo&AC8-Bar".
+   Example 3: ${imapfolder {Räksmörgås}}    yields "R&AOQ-ksm&APY-rg&AOU-s".
+
+Note that the source charset setting is vital, and also that characters
+must be representable in UTF-16.
+
+
+
+
 --------------------------------------------------------------
 End of file
 --------------------------------------------------------------
index b7413e20143aee4e24c01f0f9eed5d82a7012c94..1d5a5f6f4557619b4f237c03065f4c87341af86e 100644 (file)
@@ -295,7 +295,14 @@ convert4r4: Makefile ../src/convert4r4.src
 
 OBJ_WITH_CONTENT_SCAN = malware.o mime.o regex.o spam.o spool_mbox.o
 OBJ_WITH_OLD_DEMIME = demime.o
-OBJ_EXPERIMENTAL = bmi_spam.o spf.o srs.o dcc.o dmarc.o dane.o utf8.o
+OBJ_EXPERIMENTAL = bmi_spam.o \
+                               dane.o \
+                               dcc.o \
+                               dmarc.o \
+                               imap_utf7.o \
+                               spf.o \
+                               srs.o \
+                               utf8.o
 
 # Targets for final binaries; the main one has a build number which is
 # updated each time. We don't bother with that for the auxiliaries.
@@ -618,13 +625,14 @@ demime.o:        $(HDRS) demime.c
 
 # Dependencies for EXPERIMENTAL_* modules
 
-bmi_spam.o:      $(HDRS) bmi_spam.c
-dane.o:                 $(HDRS) dane.c dane-gnu.c dane-openssl.c
-dcc.o:           $(HDRS) dcc.h dcc.c
-dmarc.o:         $(HDRS) dmarc.h dmarc.c
-spf.o:           $(HDRS) spf.h spf.c
-srs.o:           $(HDRS) srs.h srs.c
-utf8.o:                 $(HDRS) utf8.c
+bmi_spam.o:            $(HDRS) bmi_spam.c
+dane.o:                        $(HDRS) dane.c dane-gnu.c dane-openssl.c
+dcc.o:                 $(HDRS) dcc.h dcc.c
+dmarc.o:               $(HDRS) dmarc.h dmarc.c
+imap_utf7.o:   $(HDRS) imap_utf7.c
+spf.o:                 $(HDRS) spf.h spf.c
+srs.o:                 $(HDRS) srs.h srs.c
+utf8.o:                        $(HDRS) utf8.c
 
 # The module containing tables of available lookups, routers, auths, and
 # transports must be rebuilt if any of them are. However, because the makefiles
index f9cc27c2e23440ad5f8b21cbd95bd48fc1e7a6e0..2ec572db5082ca05db9d7470c44a9e9acbb3041d 100755 (executable)
@@ -266,16 +266,17 @@ ln -s ../src/demime.h          demime.h
 # EXPERIMENTAL_*
 ln -s ../src/bmi_spam.c        bmi_spam.c
 ln -s ../src/bmi_spam.h        bmi_spam.h
-ln -s ../src/spf.c             spf.c
-ln -s ../src/spf.h             spf.h
-ln -s ../src/srs.c             srs.c
-ln -s ../src/srs.h             srs.h
 ln -s ../src/dcc.c             dcc.c
 ln -s ../src/dcc.h             dcc.h
 ln -s ../src/dane.c            dane.c
 ln -s ../src/dane-gnu.c        dane-gnu.c
 ln -s ../src/dane-openssl.c    dane-openssl.c
 ln -s ../src/danessl.h         danessl.h
+ln -s ../src/imap_utf7.c       imap_utf7.c
+ln -s ../src/spf.c             spf.c
+ln -s ../src/spf.h             spf.h
+ln -s ../src/srs.c             srs.c
+ln -s ../src/srs.h             srs.h
 ln -s ../src/utf8.c            utf8.c
 
 
index ad97f6fef15375c96880ee6ca86c2155a711d4cb..2092701630b254008d68dfdef81dd869ebd6f131 100644 (file)
@@ -114,6 +114,9 @@ static uschar *item_table[] = {
   US"hash",
   US"hmac",
   US"if",
+#ifdef EXPERIMENTAL_INTERNATIONAL
+  US"imapfolder",
+#endif
   US"length",
   US"listextract",
   US"lookup",
@@ -140,6 +143,9 @@ enum {
   EITEM_HASH,
   EITEM_HMAC,
   EITEM_IF,
+#ifdef EXPERIMENTAL_INTERNATIONAL
+  EITEM_IMAPFOLDER,
+#endif
   EITEM_LENGTH,
   EITEM_LISTEXTRACT,
   EITEM_LOOKUP,
@@ -4070,6 +4076,45 @@ while (*s != 0)
       continue;
       }
 
+#ifdef EXPERIMENTAL_INTERNATIONAL
+    case EITEM_IMAPFOLDER:
+      {                                /* ${imapfolder {name}{sep]{specials}} */
+      uschar *sub_arg[3];
+      uschar *encoded;
+
+      switch(read_subs(sub_arg, 3, 1, &s, skipping, TRUE, name, &resetok))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+
+      if (sub_arg[1] == NULL)          /* One argument */
+       {
+       sub_arg[1] = "/";               /* default separator */
+       sub_arg[2] = NULL;
+       }
+      else if (sub_arg[2] == NULL)     /* Two arguments */
+       sub_arg[2] = NULL;
+
+      if (Ustrlen(sub_arg[1]) != 1)
+       {
+       expand_string_message = 
+         string_sprintf(
+               "IMAP folder separator must be one character, found \"%s\"", 
+               sub_arg[1]);
+       goto EXPAND_FAILED;
+       }
+
+      if (!(encoded = imap_utf7_encode(sub_arg[0], headers_charset,
+                         sub_arg[1][0], sub_arg[2], &expand_string_message)))
+       goto EXPAND_FAILED;
+      if (!skipping)
+       yield = string_cat(yield, &size, &ptr, encoded, Ustrlen(encoded));
+      continue;
+      }
+#endif
+
     /* Handle database lookups unless locked out. If "skipping" is TRUE, we are
     expanding an internal string that isn't actually going to be used. All we
     need to do is check the syntax, so don't do a lookup at all. Preserve the
index 1708b7a076e7ad8a81ac4561fdd703b7db9fba8b..74198a52c5787828fa0035925ce21e078ecf853e 100644 (file)
@@ -210,6 +210,9 @@ extern int     host_nmtoa(int, int *, int, uschar *, int);
 extern uschar *host_ntoa(int, const void *, uschar *, int *);
 extern int     host_scan_for_local_hosts(host_item *, host_item **, BOOL *);
 
+extern uschar *imap_utf7_encode(uschar *, const uschar *, 
+                                uschar, uschar *, uschar **);
+
 extern void    invert_address(uschar *, uschar *);
 extern int     ip_addr(void *, int, const uschar *, int);
 extern int     ip_bind(int, int, uschar *, int);
diff --git a/src/src/imap_utf7.c b/src/src/imap_utf7.c
new file mode 100644 (file)
index 0000000..10cc1f7
--- /dev/null
@@ -0,0 +1,210 @@
+#include "exim.h"
+
+#ifdef EXPERIMENTAL_INTERNATIONAL
+
+uschar *
+imap_utf7_encode(uschar *string, const uschar *charset, uschar sep,
+  uschar *specials, uschar **error)
+{
+static uschar encode_base64[64] =
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
+int ptr = 0;
+int size = 0;
+size_t slen;
+uschar *sptr, *yield = NULL;
+int i, j;
+uschar c;
+BOOL base64mode = FALSE;
+BOOL lastsep = FALSE;
+uschar utf16buf[256];
+uschar *utf16ptr;
+uschar *s;
+uschar outbuf[256];
+uschar *outptr = outbuf;
+#if HAVE_ICONV
+iconv_t icd;
+#endif
+
+if (!specials) specials = "";
+
+/* Pass over the string. If it consists entirely of "normal" characters
+   (possibly with leading seps), return it as is. */
+for (s = string; *s; s++)
+  {
+  if (s == string && *s == sep)
+    string++;
+  if (  *s >= 0x7f
+     || *s < 0x20
+     || strchr("./&", *s)
+     || *s == sep
+     || strchr(specials, *s)
+     )
+    break;
+  }
+
+if (!*s)
+  return string;
+
+sptr = string;
+slen = Ustrlen(string);
+
+#if HAVE_ICONV
+if ((icd = iconv_open(US"UTF-16BE", charset)) == (iconv_t)-1)
+  {
+  *error = string_sprintf(
+       "imapfolder: iconv_open(\"UTF-16BE\", \"%s\") failed: %s%s",
+    charset, strerror(errno),
+    errno == EINVAL ? " (maybe unsupported conversion)" : "");
+  return NULL;
+  }
+#endif  
+
+while (slen > 0)
+  {
+#if HAVE_ICONV
+  size_t left = sizeof(utf16buf);
+  utf16ptr = utf16buf;
+
+  if (  iconv(icd, (ICONV_ARG2_TYPE)&sptr, &slen, CSS &utf16ptr, &left)
+               == (size_t)-1
+     && errno != E2BIG
+        )
+    {
+    *error = string_sprintf("imapfolder: iconv() failed to convert from %s: %s",
+                             charset, strerror(errno));
+    iconv_close(icd);
+    return NULL;
+    }
+#else
+  for (utf16ptr = utf16buf; 
+       slen > 0 && (utf16ptr - utf16buf) < sizeof(utf16buf);
+       utf16ptr += 2, slen--, sptr++)
+    {
+    *utf16ptr = *sptr;
+    *(utf16ptr+1) = '\0';
+    }
+#endif
+
+  s = utf16buf;
+  while (s < utf16ptr)
+    {
+    /* Now encode utf16buf as modified UTF-7 */
+    if (  s[0] != 0
+       || s[1] >= 0x7f
+       || s[1] < 0x20
+       || (strchr(specials, s[1]) && s[1] != sep)
+       )
+      {
+      lastsep = FALSE;
+      /* Encode as modified BASE64 */
+      if (!base64mode) 
+        {
+        *outptr++ = '&';
+        base64mode = TRUE;
+        i = 0;
+        }
+
+      for (j = 0; j < 2; j++, s++) switch (i++) 
+       {
+       case 0:
+         /* Top 6 bits of the first octet */
+         *outptr++ = encode_base64[(*s >> 2) & 0x3F];
+         c = (*s & 0x03); break;
+       case 1:
+         /* Bottom 2 bits of the first octet, and top 4 bits of the second */
+         *outptr++ = encode_base64[(c << 4) | ((*s >> 4) & 0x0F)];
+         c = (*s & 0x0F); break;
+       case 2:
+         /* Bottom 4 bits of the second octet and top 2 bits of the third */
+         *outptr++ = encode_base64[(c << 2) | ((*s >> 6) & 0x03)];
+         /* Bottom 6 bits of the third octet */
+         *outptr++ = encode_base64[*s & 0x3F];
+         i = 0;
+       }
+      }
+
+    else if (  (s[1] != '.' && s[1] != '/')
+           || s[1] == sep
+           )
+      {
+      /* Encode as self (almost) */
+      if (base64mode)
+        {
+        switch (i) 
+          {
+          case 1:
+               /* Remaining bottom 2 bits of the last octet */
+               *outptr++ = encode_base64[c << 4];
+               break;
+         case 2:
+               /* Remaining bottom 4 bits of the last octet */
+               *outptr++ = encode_base64[c << 2];
+         }
+       *outptr++ = '-';
+       base64mode = FALSE;
+       }
+
+      if (*++s == sep)
+       {
+       if (!lastsep)
+         {
+         *outptr++ = '.';
+         lastsep = TRUE;
+         }
+       }
+      else
+        {
+        *outptr++ = *s;
+        if (*s == '&')
+         *outptr++ = '-';
+       lastsep = FALSE;
+        }
+
+      s++;
+      }
+    else
+      {
+      *error = string_sprintf("imapfolder: illegal character '%c'", s[1]);
+      if (yield) store_reset(yield);
+      return NULL;
+      }
+
+    if (outptr > outbuf + sizeof(outbuf) - 3)
+      {
+      yield = string_cat(yield, &size, &ptr, outbuf, outptr - outbuf);
+      outptr = outbuf;
+      }
+
+    }
+  } /* End of input string */  
+
+if (base64mode) 
+  {
+  switch (i) 
+    {
+    case 1:
+      /* Remaining bottom 2 bits of the last octet */
+      *outptr++ = encode_base64[c << 4];
+      break;
+    case 2:
+      /* Remaining bottom 4 bits of the last octet */
+      *outptr++ = encode_base64[c << 2];
+    }
+  *outptr++ = '-';
+  }
+
+#if HAVE_ICONV
+iconv_close(icd);
+#endif
+
+yield = string_cat(yield, &size, &ptr, outbuf, outptr - outbuf);
+if (yield[ptr-1] == '.')
+  ptr--;
+yield[ptr] = '\0';
+
+return yield;
+}
+
+#endif /* whole file */
+/* vi: aw ai sw=2
+*/
index bce8e551f5a695e4de3dbd2a259505593aa8d9c0..c0bf41aa88b5bfced2be35cd85331398cce5cb65 100644 (file)
@@ -7,6 +7,8 @@ gecos_name = CALLER_NAME
 
 # ----- Main settings -----
 
+headers_charset = UTF8
+
 # ----- ACL -----
 
 # End
index 48918b702b0af560cff644d24f9ddf874e801044..481a5053fb7565170c96c333ea58b60b439db0ec 100644 (file)
@@ -1,4 +1,4 @@
-# Internationalised mail: expansions
+# Internationalisation: expansions
 #
 # Sample strings taken from RFC3942
 
@@ -93,4 +93,16 @@ ${utf8_domain_from_alabel:russian.xn--b1abfaaepdrnnbgefbaDotcwatmq2g4l.com}
 ${utf8_domain_from_alabel:spanish.xn--PorqunopuedensimplementehablarenEspaol-fmd56a.com}
 ${utf8_domain_from_alabel:vietnamese.xn--TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g.com}
 
+===========
+
+${imapfolder {Foo/Bar}}
+Foo.Bar
+
+${imapfolder {Foo/Bar} {.} {/}}
+Foo&AC8-Bar
+
+${imapfolder{Räksmörgås}}
+R&AOQ-ksm&APY-rg&AOU-s
+
+
 ****
index 1cfb7a9db86ebcd6fd403f8b2662c401cad07f1b..9e2c4bbc53082ed1e7b77fc81431acebc75f7ab9 100644 (file)
 > spanish.PorquénopuedensimplementehablarenEspañol.com
 > vietnamese.TạisaohọkhôngthểchỉnóitiếngViệt.com
 > 
+> ===========
+> 
+> Foo.Bar
+> Foo.Bar
+> 
+> Foo&AC8-Bar
+> Foo&AC8-Bar
+> 
+> R&AOQ-ksm&APY-rg&AOU-s
+> R&AOQ-ksm&APY-rg&AOU-s
+> 
+> 
 >