libemail-address-xs-perl/dovecot-parser.c

1567 lines
39 KiB
C

/*
* Copyright (c) 2002-2018 Dovecot authors
* Copyright (c) 2015-2018 Pali <pali@cpan.org>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "dovecot-parser.h"
#ifndef SIZE_MAX
#define SIZE_MAX ((size_t)-1)
#endif
void i_panic(const char *format, ...);
#ifdef DEBUG
#define i_assert(expr) \
do { if (!(expr)) \
i_panic("file %s: line %d (%s): assertion failed: (%s)", \
__FILE__, \
__LINE__, \
__FUNCTION__, \
#expr); \
} while ( 0 )
#else
#define i_assert(expr)
#endif
typedef struct {
char *buf;
size_t len;
size_t size;
} string_t;
struct rfc822_parser_context {
const unsigned char *data, *end;
string_t *last_comment;
/* Replace NUL characters with this string */
const char *nul_replacement_str;
};
struct message_address_parser_context {
struct rfc822_parser_context parser;
struct message_address *first_addr, *last_addr, addr;
string_t *str;
bool fill_missing, non_strict_dots, non_strict_dots_as_invalid;
};
static string_t *str_new(size_t initial_size)
{
char *buf;
string_t *str;
if (!initial_size)
initial_size = 1;
if (initial_size >= SIZE_MAX / 2)
i_panic("str_new() failed: %s", "initial_size is too big");
buf = malloc(initial_size);
if (!buf)
i_panic("malloc() failed: %s", strerror(errno));
str = malloc(sizeof(string_t));
if (!str)
i_panic("malloc() failed: %s", strerror(errno));
buf[0] = 0;
str->buf = buf;
str->len = 0;
str->size = initial_size;
return str;
}
static void str_free(string_t **str)
{
free((*str)->buf);
free(*str);
*str = NULL;
}
static const char *str_c(string_t *str)
{
return str->buf;
}
static char *str_ccopy(string_t *str)
{
char *copy;
copy = malloc(str->len+1);
if (!copy)
i_panic("malloc() failed: %s", strerror(errno));
memcpy(copy, str->buf, str->len);
copy[str->len] = 0;
return copy;
}
static size_t str_len(const string_t *str)
{
return str->len;
}
static void str_append_data(string_t *str, const void *data, size_t len)
{
char *new_buf;
size_t need_size;
need_size = str->len + len + 1;
if (len >= SIZE_MAX / 2 || need_size >= SIZE_MAX / 2)
i_panic("%s() failed: %s", __FUNCTION__, "len is too big");
if (need_size > str->size) {
str->size = 1;
while (str->size < need_size)
str->size <<= 1;
new_buf = realloc(str->buf, str->size);
if (!new_buf)
i_panic("realloc() failed: %s", strerror(errno));
str->buf = new_buf;
}
memcpy(str->buf + str->len, data, len);
str->len += len;
str->buf[str->len] = 0;
}
static void str_append(string_t *str, const char *cstr)
{
str_append_data(str, cstr, strlen(cstr));
}
static void str_append_c(string_t *str, unsigned char chr)
{
str_append_data(str, &chr, 1);
}
static void str_truncate(string_t *str, size_t len)
{
if (str->size - 1 <= len || str->len <= len)
return;
str->len = len;
str->buf[len] = 0;
}
/*
atext = ALPHA / DIGIT / ; Any character except controls,
"!" / "#" / ; SP, and specials.
"$" / "%" / ; Used for atoms
"&" / "'" /
"*" / "+" /
"-" / "/" /
"=" / "?" /
"^" / "_" /
"`" / "{" /
"|" / "}" /
"~"
MIME:
token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
or tspecials>
tspecials := "(" / ")" / "<" / ">" / "@" /
"," / ";" / ":" / "\" / <">
"/" / "[" / "]" / "?" / "="
So token is same as dot-atom, except stops also at '/', '?' and '='.
*/
/* atext chars are marked with 1, alpha and digits with 2,
atext-but-mime-tspecials with 4 */
unsigned char rfc822_atext_chars[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0-15 */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-31 */
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 4, /* 32-47 */
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 4, 0, 4, /* 48-63 */
0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 64-79 */
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, /* 80-95 */
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 96-111 */
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, /* 112-127 */
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
};
#define IS_ATEXT(c) \
(rfc822_atext_chars[(int)(unsigned char)(c)] != 0)
#define IS_ATEXT_NON_TSPECIAL(c) \
((rfc822_atext_chars[(int)(unsigned char)(c)] & 3) != 0)
/*
qtext = %d33 / ; Printable US-ASCII
%d35-91 / ; characters not including
%d93-126 / ; "\" or the quote character
obs-qtext
obs-qtext = obs-NO-WS-CTL
obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
%d11 / ; characters that do not
%d12 / ; include the carriage
%d14-31 / ; return, line feed, and
%d127 ; white space characters
So qtext is everything expects '\0', '\t', '\n', '\r', ' ', '"', '\\'.
*/
/* non-qtext characters */
#define CHAR_NEEDS_ESCAPE(c) ((c) == '"' || (c) == '\\' || (c) == '\0' || (c) == '\t' || (c) == '\n' || (c) == '\r')
/* quote with "" and escape all needed characters */
static void str_append_maybe_escape(string_t *str, const char *data, size_t len, bool quote_dot)
{
const char *p;
const char *end;
if (len == 0) {
str_append(str, "\"\"");
return;
}
/* leading or trailing dot needs to be always quoted */
if (data[0] == '.' || data[len-1] == '.')
quote_dot = true;
end = data + len;
/* see if we need to quote it */
for (p = data; p != end; p++) {
if (!IS_ATEXT(*p) && (quote_dot || *p != '.'))
break;
}
if (p == end) {
str_append_data(str, data, len);
return;
}
/* see if we need to escape it */
for (p = data; p != end; p++) {
if (CHAR_NEEDS_ESCAPE(*p))
break;
}
if (p == end) {
/* only quote */
str_append_c(str, '"');
str_append_data(str, data, len);
str_append_c(str, '"');
return;
}
/* quote and escape */
str_append_c(str, '"');
str_append_data(str, data, (size_t) (p - data));
for (; p != end; p++) {
if (CHAR_NEEDS_ESCAPE(*p))
str_append_c(str, '\\');
str_append_c(str, *p);
}
str_append_c(str, '"');
}
/* Parse given data using RFC 822 token parser. */
static void rfc822_parser_init(struct rfc822_parser_context *ctx,
const unsigned char *data, size_t size,
string_t *last_comment)
{
memset(ctx, 0, sizeof(*ctx));
ctx->data = data;
ctx->end = data + size;
ctx->last_comment = last_comment;
}
static void rfc822_parser_deinit(struct rfc822_parser_context *ctx)
{
/* make sure the parsing didn't trigger a bug that caused reading
past the end pointer. */
i_assert(ctx->data <= ctx->end);
/* make sure the parser is no longer accessed */
ctx->data = ctx->end = NULL;
}
/* The functions below return 1 = more data available, 0 = no more data
available (but a value might have been returned now), -1 = invalid input.
LWSP is automatically skipped after value, but not before it. So typically
you begin with skipping LWSP and then start using the parse functions. */
/* Parse comment. Assumes parser's data points to '(' */
static int rfc822_skip_comment(struct rfc822_parser_context *ctx)
{
const unsigned char *start;
size_t len;
int level = 1;
i_assert(*ctx->data == '(');
if (ctx->last_comment != NULL)
str_truncate(ctx->last_comment, 0);
start = ++ctx->data;
for (; ctx->data < ctx->end; ctx->data++) {
switch (*ctx->data) {
case '\0':
if (ctx->nul_replacement_str != NULL) {
if (ctx->last_comment != NULL) {
str_append_data(ctx->last_comment, start,
ctx->data - start);
str_append(ctx->last_comment,
ctx->nul_replacement_str);
start = ctx->data + 1;
}
} else {
return -1;
}
break;
case '(':
level++;
break;
case ')':
if (--level == 0) {
if (ctx->last_comment != NULL) {
str_append_data(ctx->last_comment, start,
ctx->data - start);
}
ctx->data++;
return ctx->data < ctx->end ? 1 : 0;
}
break;
case '\n':
/* folding whitespace, remove the (CR)LF */
if (ctx->last_comment == NULL)
break;
len = ctx->data - start;
if (len > 0 && start[len-1] == '\r')
len--;
str_append_data(ctx->last_comment, start, len);
start = ctx->data + 1;
break;
case '\\':
ctx->data++;
if (ctx->data >= ctx->end)
return -1;
#if 0
if (*ctx->data == '\r' || *ctx->data == '\n' ||
*ctx->data == '\0') {
/* quoted-pair doesn't allow CR/LF/NUL.
They are part of the obs-qp though, so don't
return them as error. */
ctx->data--;
break;
}
#endif
if (ctx->last_comment != NULL) {
str_append_data(ctx->last_comment, start,
ctx->data - start - 1);
}
start = ctx->data;
break;
}
}
/* missing ')' */
return -1;
}
/* Skip LWSP if there is any */
static int rfc822_skip_lwsp(struct rfc822_parser_context *ctx)
{
for (; ctx->data < ctx->end;) {
if (*ctx->data == ' ' || *ctx->data == '\t' ||
*ctx->data == '\r' || *ctx->data == '\n') {
ctx->data++;
continue;
}
if (*ctx->data != '(')
break;
if (rfc822_skip_comment(ctx) < 0)
return -1;
}
return ctx->data < ctx->end ? 1 : 0;
}
/* Stop at next non-atext char */
int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str)
{
const unsigned char *start;
/*
atom = [CFWS] 1*atext [CFWS]
atext =
; Any character except controls, SP, and specials.
*/
if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
return -1;
for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
if (IS_ATEXT(*ctx->data))
continue;
str_append_data(str, start, ctx->data - start);
return rfc822_skip_lwsp(ctx);
}
str_append_data(str, start, ctx->data - start);
return 0;
}
/* Like parse_atom() but don't stop at '.' */
static int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str, bool stop_trailing_dot)
{
const unsigned char *start;
const unsigned char *last_dot_ptr;
bool last_is_dot;
bool dot_problem;
int ret;
/*
dot-atom = [CFWS] dot-atom-text [CFWS]
dot-atom-text = 1*atext *("." 1*atext)
atext =
; Any character except controls, SP, and specials.
For RFC-822 compatibility allow LWSP around '.'
*/
if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
return -1;
last_dot_ptr = ctx->data;
last_is_dot = false;
dot_problem = false;
for (start = ctx->data++; ctx->data < ctx->end; ) {
if (IS_ATEXT(*ctx->data)) {
ctx->data++;
continue;
}
#if 0
if (start == ctx->data)
dot_problem = true;
#endif
str_append_data(str, start, ctx->data - start);
if (ctx->data - start > 0)
last_is_dot = false;
if ((ret = rfc822_skip_lwsp(ctx)) <= 0)
return (dot_problem && ret >= 0) ? -2 : ret;
if (*ctx->data != '.') {
if (last_is_dot && stop_trailing_dot) {
ctx->data = last_dot_ptr;
return dot_problem ? -2 : 1;
}
return (last_is_dot || dot_problem) ? -2 : 1;
}
if (last_is_dot)
dot_problem = true;
last_dot_ptr = ctx->data;
ctx->data++;
str_append_c(str, '.');
last_is_dot = true;
if (rfc822_skip_lwsp(ctx) <= 0)
return (dot_problem && ret >= 0) ? -2 : ret;
start = ctx->data;
}
#if 0
i_assert(start != ctx->data);
#endif
str_append_data(str, start, ctx->data - start);
return dot_problem ? -2 : 0;
}
/* "quoted string" */
static int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx, string_t *str)
{
const unsigned char *start;
bool char_problem;
int ret;
size_t len;
i_assert(ctx->data < ctx->end);
i_assert(*ctx->data == '"');
ctx->data++;
char_problem = false;
for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
switch (*ctx->data) {
case '\0':
if (ctx->nul_replacement_str != NULL) {
str_append_data(str, start, ctx->data - start);
str_append(str, ctx->nul_replacement_str);
start = ctx->data + 1;
} else {
char_problem = true;
}
break;
case '"':
str_append_data(str, start, ctx->data - start);
ctx->data++;
ret = rfc822_skip_lwsp(ctx);
return (char_problem && ret >= 0) ? -2 : ret;
case '\r':
if (ctx->data+1 < ctx->end && *(ctx->data+1) != '\n')
char_problem = true;
break;
case '\n':
#if 0
/* folding whitespace, remove the (CR)LF */
len = ctx->data - start;
if (len > 0 && start[len-1] == '\r')
len--;
str_append_data(str, start, len);
start = ctx->data + 1;
#endif
len = ctx->data - start;
if (len <= 0 || start[len-1] != '\r')
char_problem = true;
break;
case '\\':
ctx->data++;
if (ctx->data >= ctx->end)
return -1;
#if 0
if (*ctx->data == '\r' || *ctx->data == '\n' ||
*ctx->data == '\0') {
/* quoted-pair doesn't allow CR/LF/NUL.
They are part of the obs-qp though, so don't
return them as error. */
ctx->data--;
break;
}
#endif
str_append_data(str, start, ctx->data - start - 1);
str_append_c(str, *ctx->data);
start = ctx->data+1;
break;
}
}
/* missing '"' */
return -1;
}
static int
rfc822_parse_atom_or_dot(struct rfc822_parser_context *ctx, string_t *str)
{
const unsigned char *start;
/*
atom = [CFWS] 1*atext [CFWS]
atext =
; Any character except controls, SP, and specials.
The difference between this function and rfc822_parse_dot_atom()
is that this doesn't just silently skip over all the whitespace.
*/
for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
if (IS_ATEXT(*ctx->data) || *ctx->data == '.')
continue;
str_append_data(str, start, ctx->data - start);
return rfc822_skip_lwsp(ctx);
}
str_append_data(str, start, ctx->data - start);
return 0;
}
/* atom or quoted-string */
static int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str)
{
int ret;
bool char_problem;
char_problem = false;
/*
phrase = 1*word / obs-phrase
word = atom / quoted-string
obs-phrase = word *(word / "." / CFWS)
*/
if (ctx->data >= ctx->end)
return 0;
if (*ctx->data == '.')
return -1;
for (;;) {
if (*ctx->data == '"')
ret = rfc822_parse_quoted_string(ctx, str);
else
ret = rfc822_parse_atom_or_dot(ctx, str);
if (ret <= 0 && ret != -2)
return (char_problem && ret == 0) ? -2 : ret;
if (ret == -2) {
char_problem = true;
if (ctx->data >= ctx->end)
return -2;
}
if (!IS_ATEXT(*ctx->data) && *ctx->data != '"'
&& *ctx->data != '.')
break;
str_append_c(str, ' ');
}
ret = rfc822_skip_lwsp(ctx);
return (char_problem && ret >= 0) ? -2 : ret;
}
static int
rfc822_parse_domain_literal(struct rfc822_parser_context *ctx, string_t *str)
{
const unsigned char *start;
size_t len;
/*
domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
dcontent = dtext / quoted-pair
dtext = NO-WS-CTL / ; Non white space controls
%d33-90 / ; The rest of the US-ASCII
%d94-126 ; characters not including "[",
; "]", or "\"
*/
i_assert(ctx->data < ctx->end);
i_assert(*ctx->data == '[');
for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
switch (*ctx->data) {
case '\0':
if (ctx->nul_replacement_str != NULL) {
str_append_data(str, start, ctx->data - start);
str_append(str, ctx->nul_replacement_str);
start = ctx->data + 1;
} else {
return -1;
}
break;
case '[':
/* not allowed */
return -1;
case ']':
str_append_data(str, start, ctx->data - start + 1);
ctx->data++;
return rfc822_skip_lwsp(ctx);
case '\n':
/* folding whitespace, remove the (CR)LF */
len = ctx->data - start;
if (len > 0 && start[len-1] == '\r')
len--;
str_append_data(str, start, len);
start = ctx->data + 1;
break;
case '\\':
/* note: the '\' is preserved in the output */
ctx->data++;
if (ctx->data >= ctx->end)
return -1;
#if 0
if (*ctx->data == '\r' || *ctx->data == '\n' ||
*ctx->data == '\0') {
/* quoted-pair doesn't allow CR/LF/NUL.
They are part of the obs-qp though, so don't
return them as error. */
str_append_data(str, start, ctx->data - start);
start = ctx->data;
ctx->data--;
break;
}
#endif
break;
}
}
/* missing ']' */
return -1;
}
/* dot-atom / domain-literal */
static int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str)
{
/*
domain = dot-atom / domain-literal / obs-domain
domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
obs-domain = atom *("." atom)
*/
i_assert(ctx->data < ctx->end);
i_assert(*ctx->data == '@');
ctx->data++;
if (rfc822_skip_lwsp(ctx) <= 0)
return -1;
if (*ctx->data == '[')
return rfc822_parse_domain_literal(ctx, str);
else
return rfc822_parse_dot_atom(ctx, str, false);
}
static void add_address(struct message_address_parser_context *ctx)
{
struct message_address *addr;
addr = malloc(sizeof(struct message_address));
if (!addr)
i_panic("malloc() failed: %s", strerror(errno));
memcpy(addr, &ctx->addr, sizeof(ctx->addr));
memset(&ctx->addr, 0, sizeof(ctx->addr));
if (ctx->first_addr == NULL)
ctx->first_addr = addr;
else
ctx->last_addr->next = addr;
ctx->last_addr = addr;
}
static int
parse_nonstrict_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
{
int ret = -1;
do {
while (*ctx->data == '.') {
str_append_c(str, '.');
ctx->data++;
if (ctx->data == ctx->end) {
/* @domain is missing, but local-part
parsing was successful */
return 0;
}
ret = 1;
}
if (*ctx->data == '@')
break;
ret = rfc822_parse_atom(ctx, str);
} while (ret > 0 && *ctx->data == '.');
return ret;
}
static int parse_local_part(struct message_address_parser_context *ctx)
{
int ret;
bool char_problem;
/*
local-part = dot-atom / quoted-string / obs-local-part
obs-local-part = word *("." word)
*/
i_assert(ctx->parser.data < ctx->parser.end);
str_truncate(ctx->str, 0);
char_problem = false;
while (ctx->parser.data < ctx->parser.end) {
if (*ctx->parser.data == '"')
ret = rfc822_parse_quoted_string(&ctx->parser, ctx->str);
else if (!ctx->non_strict_dots || ctx->non_strict_dots_as_invalid)
ret = rfc822_parse_dot_atom(&ctx->parser, ctx->str, true);
else
ret = parse_nonstrict_dot_atom(&ctx->parser, ctx->str);
if (ret < 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid)))
return -1;
if (ret == -2)
char_problem = true;
if (ctx->parser.data >= ctx->parser.end)
break;
if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0)
break;
if (*ctx->parser.data != '.')
break;
ctx->parser.data++;
if (ctx->parser.data >= ctx->parser.end) {
char_problem = true;
break;
}
if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0)
break;
if (ctx->parser.data >= ctx->parser.end || *ctx->parser.data == '@') {
char_problem = true;
break;
}
}
if (char_problem || ret < 0)
ctx->addr.invalid_syntax = true;
ctx->addr.mailbox = str_ccopy(ctx->str);
ctx->addr.mailbox_len = str_len(ctx->str);
return ret;
}
static int parse_domain(struct message_address_parser_context *ctx)
{
int ret;
str_truncate(ctx->str, 0);
if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) < 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid)))
return -1;
ctx->addr.domain = str_ccopy(ctx->str);
ctx->addr.domain_len = str_len(ctx->str);
return ret;
}
static int parse_domain_list(struct message_address_parser_context *ctx)
{
int ret;
bool dot_problem;
/* obs-domain-list = "@" domain *(*(CFWS / "," ) [CFWS] "@" domain) */
str_truncate(ctx->str, 0);
dot_problem = false;
for (;;) {
if (ctx->parser.data >= ctx->parser.end)
return dot_problem ? -2 : 0;
if (*ctx->parser.data != '@')
break;
if (str_len(ctx->str) > 0)
str_append_c(ctx->str, ',');
str_append_c(ctx->str, '@');
if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) <= 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid)))
return ret;
if (ret == -2)
dot_problem = true;
while (rfc822_skip_lwsp(&ctx->parser) > 0 &&
*ctx->parser.data == ',')
ctx->parser.data++;
}
ctx->addr.route = str_ccopy(ctx->str);
ctx->addr.route_len = str_len(ctx->str);
return dot_problem ? -2 : 1;
}
static int parse_angle_addr(struct message_address_parser_context *ctx,
bool parsing_path)
{
int ret;
/* "<" [ "@" route ":" ] local-part "@" domain ">" */
i_assert(*ctx->parser.data == '<');
ctx->parser.data++;
if (rfc822_skip_lwsp(&ctx->parser) <= 0)
return -1;
if (*ctx->parser.data == '@') {
if ((ret = parse_domain_list(ctx)) > 0 && *ctx->parser.data == ':') {
ctx->parser.data++;
} else if (parsing_path && (ctx->parser.data >= ctx->parser.end || *ctx->parser.data != ':')) {
return -1;
} else {
if (ctx->fill_missing && ret != -2)
ctx->addr.route = strdup("INVALID_ROUTE");
ctx->addr.invalid_syntax = true;
if (ctx->parser.data >= ctx->parser.end)
return -1;
if (ret == -2)
ctx->parser.data++;
/* try to continue anyway */
}
if (rfc822_skip_lwsp(&ctx->parser) <= 0)
return -1;
}
if (*ctx->parser.data == '>') {
/* <> address isn't valid */
} else {
if ((ret = parse_local_part(ctx)) <= 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid)))
return -1;
if (ret == -2)
ctx->addr.invalid_syntax = true;
if (ctx->parser.data >= ctx->parser.end)
return 0;
if (*ctx->parser.data == '@') {
if ((ret = parse_domain(ctx)) <= 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid)))
return -1;
if (ret == -2)
ctx->addr.invalid_syntax = true;
if (ctx->parser.data >= ctx->parser.end)
return 0;
}
}
if (*ctx->parser.data != '>')
return -1;
ctx->parser.data++;
return rfc822_skip_lwsp(&ctx->parser);
}
static int parse_name_addr(struct message_address_parser_context *ctx)
{
int ret;
/*
name-addr = [display-name] angle-addr
display-name = phrase
*/
str_truncate(ctx->str, 0);
ret = rfc822_parse_phrase(&ctx->parser, ctx->str);
if ((ret <= 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid))) ||
*ctx->parser.data != '<')
return -1;
if (ret == -2)
ctx->addr.invalid_syntax = true;
if (str_len(ctx->str) == 0) {
/* Cope with "<address>" without display name */
ctx->addr.name = NULL;
} else {
ctx->addr.name = str_ccopy(ctx->str);
ctx->addr.name_len = str_len(ctx->str);
}
if (ctx->parser.last_comment != NULL)
str_truncate(ctx->parser.last_comment, 0);
if (parse_angle_addr(ctx, false) < 0) {
/* broken */
if (ctx->fill_missing)
ctx->addr.domain = strdup("SYNTAX_ERROR");
ctx->addr.invalid_syntax = true;
}
if (ctx->parser.last_comment != NULL) {
if (str_len(ctx->parser.last_comment) > 0) {
ctx->addr.comment =
str_ccopy(ctx->parser.last_comment);
ctx->addr.comment_len =
str_len(ctx->parser.last_comment);
}
}
return ctx->parser.data < ctx->parser.end ? 1 : 0;
}
static int parse_addr_spec(struct message_address_parser_context *ctx)
{
/* addr-spec = local-part "@" domain */
int ret, ret2 = -3;
i_assert(ctx->parser.data < ctx->parser.end);
if (ctx->parser.last_comment != NULL)
str_truncate(ctx->parser.last_comment, 0);
#if 0
bool quoted_string = *ctx->parser.data == '"';
#endif
ret = parse_local_part(ctx);
if (ret <= 0) {
/* end of input or parsing local-part failed */
ctx->addr.invalid_syntax = true;
}
if (ret != 0 && ctx->parser.data < ctx->parser.end &&
*ctx->parser.data == '@') {
ret2 = parse_domain(ctx);
if (ret2 <= 0 && ret != -2)
ret = ret2;
if (ret2 == -2) {
ctx->addr.invalid_syntax = true;
if (ctx->parser.data >= ctx->parser.end)
ret = 0;
}
}
if (ctx->parser.last_comment != NULL && str_len(ctx->parser.last_comment) > 0) {
ctx->addr.comment = str_ccopy(ctx->parser.last_comment);
ctx->addr.comment_len = str_len(ctx->parser.last_comment);
} else if (ret2 == -3) {
#if 0
/* So far we've read user without @domain and without
(Display Name). We'll assume that a single "user" (already
read into addr.mailbox) is a mailbox, but if it's followed
by anything else it's a display-name. */
str_append_c(ctx->str, ' ');
size_t orig_str_len = str_len(ctx->str);
(void)rfc822_parse_phrase(&ctx->parser, ctx->str);
if (str_len(ctx->str) != orig_str_len) {
ctx->addr.mailbox = NULL;
ctx->addr.name = str_ccopy(ctx->str);
ctx->addr.name_len = str_len(ctx->str);
} else {
if (!quoted_string)
ctx->addr.domain = strdup("");
}
ctx->addr.invalid_syntax = true;
ret = -1;
#endif
}
return ret;
}
static void add_fixed_address(struct message_address_parser_context *ctx)
{
if (ctx->addr.mailbox == NULL) {
ctx->addr.mailbox = strdup(!ctx->fill_missing ? "" : "MISSING_MAILBOX");
ctx->addr.invalid_syntax = true;
}
if (ctx->addr.domain == NULL || ctx->addr.domain_len == 0) {
free(ctx->addr.domain);
ctx->addr.domain = strdup(!ctx->fill_missing ? "" : "MISSING_DOMAIN");
ctx->addr.invalid_syntax = true;
}
add_address(ctx);
}
static int parse_mailbox(struct message_address_parser_context *ctx)
{
const unsigned char *start;
size_t len;
int ret;
/* mailbox = name-addr / addr-spec */
start = ctx->parser.data;
if ((ret = parse_name_addr(ctx)) < 0) {
/* nope, should be addr-spec */
if (ctx->addr.name != NULL) {
free(ctx->addr.name);
ctx->addr.name = NULL;
}
if (ctx->addr.route != NULL) {
free(ctx->addr.route);
ctx->addr.route = NULL;
}
if (ctx->addr.mailbox != NULL) {
free(ctx->addr.mailbox);
ctx->addr.mailbox = NULL;
}
if (ctx->addr.domain != NULL) {
free(ctx->addr.domain);
ctx->addr.domain = NULL;
}
if (ctx->addr.comment != NULL) {
free(ctx->addr.comment);
ctx->addr.comment = NULL;
}
if (ctx->addr.original != NULL) {
free(ctx->addr.original);
ctx->addr.original = NULL;
}
ctx->parser.data = start;
ret = parse_addr_spec(ctx);
if (ctx->addr.invalid_syntax && ctx->addr.name == NULL &&
ctx->addr.mailbox != NULL && ctx->addr.domain == NULL) {
ctx->addr.name = ctx->addr.mailbox;
ctx->addr.name_len = ctx->addr.mailbox_len;
ctx->addr.mailbox = NULL;
ctx->addr.mailbox_len = 0;
}
}
if (ret < 0)
ctx->addr.invalid_syntax = true;
len = ctx->parser.data - start;
ctx->addr.original = malloc(len + 1);
if (!ctx->addr.original)
i_panic("malloc() failed: %s", strerror(errno));
memcpy(ctx->addr.original, start, len);
ctx->addr.original[len] = 0;
ctx->addr.original_len = len;
add_fixed_address(ctx);
free(ctx->addr.original);
ctx->addr.original = NULL;
return ret;
}
static int parse_group(struct message_address_parser_context *ctx)
{
int ret;
/*
group = display-name ":" [mailbox-list / CFWS] ";" [CFWS]
display-name = phrase
*/
str_truncate(ctx->str, 0);
ret = rfc822_parse_phrase(&ctx->parser, ctx->str);
if ((ret <= 0 && (ret != -2 || (!ctx->non_strict_dots && !ctx->non_strict_dots_as_invalid))) ||
*ctx->parser.data != ':')
return -1;
if (ret == -2)
ctx->addr.invalid_syntax = true;
/* from now on don't return -1 even if there are problems, so that
the caller knows this is a group */
ctx->parser.data++;
if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0)
ctx->addr.invalid_syntax = true;
ctx->addr.mailbox = str_ccopy(ctx->str);
ctx->addr.mailbox_len = str_len(ctx->str);
add_address(ctx);
if (ret > 0 && *ctx->parser.data != ';') {
for (;;) {
/* mailbox-list =
(mailbox *("," mailbox)) / obs-mbox-list */
if (parse_mailbox(ctx) <= 0) {
/* broken mailbox - try to continue anyway. */
}
if (ctx->parser.data >= ctx->parser.end ||
*ctx->parser.data != ',')
break;
ctx->parser.data++;
if (rfc822_skip_lwsp(&ctx->parser) <= 0) {
ret = -1;
break;
}
}
}
if (ret >= 0) {
if (ctx->parser.data >= ctx->parser.end ||
*ctx->parser.data != ';')
ret = -1;
else {
ctx->parser.data++;
ret = rfc822_skip_lwsp(&ctx->parser);
}
}
if (ret < 0)
ctx->addr.invalid_syntax = true;
add_address(ctx);
return ret == 0 ? 0 : 1;
}
static int parse_address(struct message_address_parser_context *ctx)
{
const unsigned char *start;
int ret;
/* address = mailbox / group */
start = ctx->parser.data;
if ((ret = parse_group(ctx)) < 0) {
/* not a group, try mailbox */
ctx->parser.data = start;
ret = parse_mailbox(ctx);
}
return ret;
}
static int parse_address_list(struct message_address_parser_context *ctx,
unsigned int max_addresses)
{
const unsigned char *start;
size_t len;
int ret = 0;
/* address-list = (address *("," address)) / obs-addr-list */
while (max_addresses > 0) {
max_addresses--;
if ((ret = parse_address(ctx)) == 0)
break;
if (ctx->parser.data >= ctx->parser.end ||
*ctx->parser.data != ',') {
ctx->last_addr->invalid_syntax = true;
ret = -1;
break;
}
ctx->parser.data++;
start = ctx->parser.data;
if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0) {
if (ret < 0) {
/* ends with some garbage */
len = ctx->parser.data - start;
ctx->addr.original = malloc(len + 1);
if (!ctx->addr.original)
i_panic("malloc() failed: %s", strerror(errno));
memcpy(ctx->addr.original, start, len);
ctx->addr.original[len] = 0;
ctx->addr.original_len = len;
add_fixed_address(ctx);
free(ctx->addr.original);
ctx->addr.original = NULL;
}
break;
}
}
return ret;
}
static char *mem_copy(const char *mem, size_t len)
{
char *copy;
copy = malloc(len+1);
if (!copy)
i_panic("malloc() failed: %s", strerror(errno));
memcpy(copy, mem, len);
copy[len] = 0;
return copy;
}
void message_address_add(struct message_address **first, struct message_address **last,
const char *name, size_t name_len, const char *route, size_t route_len,
const char *mailbox, size_t mailbox_len, const char *domain, size_t domain_len,
const char *comment, size_t comment_len)
{
struct message_address *message;
message = malloc(sizeof(struct message_address));
if (!message)
i_panic("malloc() failed: %s", strerror(errno));
message->name = name ? mem_copy(name, name_len) : NULL;
message->name_len = name_len;
message->route = route ? mem_copy(route, route_len) : NULL;
message->route_len = route_len;
message->mailbox = mailbox ? mem_copy(mailbox, mailbox_len) : NULL;
message->mailbox_len = mailbox_len;
message->domain = domain ? mem_copy(domain, domain_len) : NULL;
message->domain_len = domain_len;
message->comment = comment ? mem_copy(comment, comment_len) : NULL;
message->comment_len = comment_len;
message->original = NULL;
message->original_len = 0;
message->next = NULL;
if (!*first)
*first = message;
else
(*last)->next = message;
*last = message;
}
void message_address_free(struct message_address **addr)
{
struct message_address *current;
struct message_address *next;
current = *addr;
while (current) {
next = current->next;
free(current->name);
free(current->route);
free(current->mailbox);
free(current->domain);
free(current->comment);
free(current->original);
free(current);
current = next;
}
*addr = NULL;
}
struct message_address *
message_address_parse(const char *input, size_t input_len,
unsigned int max_addresses,
enum message_address_parse_flags flags)
{
string_t *str;
struct message_address_parser_context ctx;
memset(&ctx, 0, sizeof(ctx));
str = str_new(128);
rfc822_parser_init(&ctx.parser, (const unsigned char *)input, input_len, str);
if (rfc822_skip_lwsp(&ctx.parser) <= 0) {
/* no addresses */
str_free(&str);
return NULL;
}
ctx.str = str_new(128);
ctx.fill_missing = (flags & MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) != 0;
ctx.non_strict_dots = (flags & MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS) == 0;
ctx.non_strict_dots_as_invalid = (flags & MESSAGE_ADDRESS_PARSE_FLAG_NON_STRICT_DOTS_AS_INVALID) != 0;
(void)parse_address_list(&ctx, max_addresses);
str_free(&ctx.str);
str_free(&str);
rfc822_parser_deinit(&ctx.parser);
return ctx.first_addr;
}
static bool has_mime_word(const char *str, size_t len)
{
const char *ptr;
const char *end;
ptr = str;
end = str+len;
while ((ptr = memchr(ptr, '=', end - ptr)) != NULL) {
ptr++;
if (*ptr == '?')
return true;
}
return false;
}
void message_address_write(char **output, size_t *output_len, const struct message_address *addr)
{
string_t *str;
const char *tmp;
bool first = true, in_group = false;
str = str_new(128);
#if 0
if (addr == NULL)
return;
/* <> path */
if (addr->mailbox == NULL && addr->domain == NULL) {
i_assert(addr->next == NULL);
str_append(str, "<>");
return;
}
#endif
/* a) mailbox@domain
b) name <@route:mailbox@domain>
c) group: .. ; */
while (addr != NULL) {
if (first)
first = false;
else
str_append(str, ", ");
if (addr->domain == NULL) {
if (!in_group) {
/* beginning of group. mailbox is the group
name, others are NULL. */
if (addr->mailbox != NULL && addr->mailbox_len != 0) {
/* check for MIME encoded-word */
if (has_mime_word(addr->mailbox, addr->mailbox_len))
/* MIME encoded-word MUST NOT appear within a 'quoted-string'
so escaping and quoting of phrase is not possible, instead
use obsolete RFC822 phrase syntax which allow spaces */
str_append_data(str, addr->mailbox, addr->mailbox_len);
else
str_append_maybe_escape(str, addr->mailbox, addr->mailbox_len, true);
} else {
/* empty group name needs to be quoted */
str_append(str, "\"\"");
}
str_append(str, ": ");
first = true;
} else {
/* end of group. all fields should be NULL. */
i_assert(addr->mailbox == NULL);
/* cut out the ", " */
tmp = str_c(str)+str_len(str)-2;
i_assert((tmp[0] == ',' || tmp[0] == ':') && tmp[1] == ' ');
if (tmp[0] == ',' && tmp[1] == ' ')
str_truncate(str, str_len(str)-2);
else if (tmp[0] == ':' && tmp[1] == ' ')
str_truncate(str, str_len(str)-1);
str_append_c(str, ';');
}
in_group = !in_group;
} else if ((addr->name == NULL || addr->name_len == 0) &&
addr->route == NULL) {
/* no name and no route. use only mailbox@domain */
i_assert(addr->mailbox != NULL);
str_append_maybe_escape(str, addr->mailbox, addr->mailbox_len, false);
str_append_c(str, '@');
str_append_data(str, addr->domain, addr->domain_len);
if (addr->comment != NULL) {
str_append(str, " (");
str_append_data(str, addr->comment, addr->comment_len);
str_append_c(str, ')');
}
} else {
/* name and/or route. use full <mailbox@domain> Name */
i_assert(addr->mailbox != NULL);
if (addr->name != NULL && addr->name_len != 0) {
/* check for MIME encoded-word */
if (has_mime_word(addr->name, addr->name_len))
/* MIME encoded-word MUST NOT appear within a 'quoted-string'
so escaping and quoting of phrase is not possible, instead
use obsolete RFC822 phrase syntax which allow spaces */
str_append_data(str, addr->name, addr->name_len);
else
str_append_maybe_escape(str, addr->name, addr->name_len, true);
}
if (addr->route != NULL ||
addr->mailbox_len != 0 ||
addr->domain_len != 0) {
if (addr->name != NULL && addr->name_len != 0)
str_append_c(str, ' ');
str_append_c(str, '<');
if (addr->route != NULL) {
str_append_data(str, addr->route, addr->route_len);
str_append_c(str, ':');
}
str_append_maybe_escape(str, addr->mailbox, addr->mailbox_len, false);
if (addr->domain_len != 0) {
str_append_c(str, '@');
str_append_data(str, addr->domain, addr->domain_len);
}
str_append_c(str, '>');
}
if (addr->comment != NULL) {
str_append(str, " (");
str_append_data(str, addr->comment, addr->comment_len);
str_append_c(str, ')');
}
}
addr = addr->next;
}
*output = str_ccopy(str);
*output_len = str_len(str);
str_free(&str);
}
void compose_address(char **output, size_t *output_len, const char *mailbox, size_t mailbox_len, const char *domain, size_t domain_len)
{
string_t *str;
str = str_new(128);
str_append_maybe_escape(str, mailbox, mailbox_len, false);
str_append_c(str, '@');
str_append_data(str, domain, domain_len);
*output = str_ccopy(str);
*output_len = str_len(str);
str_free(&str);
}
void split_address(const char *input, size_t input_len, char **mailbox, size_t *mailbox_len, char **domain, size_t *domain_len)
{
struct message_address_parser_context ctx;
int ret;
if (!input || !input[0]) {
*mailbox = NULL;
*mailbox_len = 0;
*domain = NULL;
*domain_len = 0;
return;
}
memset(&ctx, 0, sizeof(ctx));
rfc822_parser_init(&ctx.parser, (const unsigned char *)input, input_len, NULL);
ctx.str = str_new(128);
ctx.fill_missing = false;
ctx.non_strict_dots = false;
ctx.non_strict_dots_as_invalid = false;
ret = rfc822_skip_lwsp(&ctx.parser);
if (ret > 0)
ret = parse_addr_spec(&ctx);
else
ret = -1;
if (ret >= 0)
ret = rfc822_skip_lwsp(&ctx.parser);
if (ret < 0 || ctx.parser.data != ctx.parser.end || ctx.addr.invalid_syntax) {
free(ctx.addr.mailbox);
free(ctx.addr.domain);
*mailbox = NULL;
*mailbox_len = 0;
*domain = NULL;
*domain_len = 0;
} else {
*mailbox = ctx.addr.mailbox;
*mailbox_len = ctx.addr.mailbox_len;
*domain = ctx.addr.domain;
*domain_len = ctx.addr.domain_len;
}
free(ctx.addr.comment);
free(ctx.addr.route);
free(ctx.addr.name);
free(ctx.addr.original);
rfc822_parser_deinit(&ctx.parser);
str_free(&ctx.str);
}
void string_free(char *string)
{
free(string);
}