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
------------
-SMTPUTF8
+INTERNATIONAL
------------------------------------------------------------
+SMTPUTF8
Internationalised mail name handling.
RFCs 6530, 6533, 5890
- 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
--------------------------------------------------------------
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.
# 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
# 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
US"hash",
US"hmac",
US"if",
+#ifdef EXPERIMENTAL_INTERNATIONAL
+ US"imapfolder",
+#endif
US"length",
US"listextract",
US"lookup",
EITEM_HASH,
EITEM_HMAC,
EITEM_IF,
+#ifdef EXPERIMENTAL_INTERNATIONAL
+ EITEM_IMAPFOLDER,
+#endif
EITEM_LENGTH,
EITEM_LISTEXTRACT,
EITEM_LOOKUP,
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
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);
--- /dev/null
+#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
+*/
# ----- Main settings -----
+headers_charset = UTF8
+
# ----- ACL -----
# End
-# Internationalised mail: expansions
+# Internationalisation: expansions
#
# Sample strings taken from RFC3942
${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
+
+
****
> 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
+>
+>
>