Example #1
0
static void mime_state_downgrade(MIME_STATE *state, int rec_type,
				         const char *text, int len)
{
    static char hexchars[] = "0123456789ABCDEF";
    const unsigned char *cp;
    int     ch;

#define QP_ENCODE(buffer, ch) { \
	VSTRING_ADDCH(buffer, '='); \
	VSTRING_ADDCH(buffer, hexchars[(ch >> 4) & 0xff]); \
	VSTRING_ADDCH(buffer, hexchars[ch & 0xf]); \
    }

    /*
     * Insert a soft line break when the output reaches a critical length
     * before we reach a hard line break.
     */
    for (cp = CU_CHAR_PTR(text); cp < CU_CHAR_PTR(text + len); cp++) {
	/* Critical length before hard line break. */
	if (LEN(state->output_buffer) > 72) {
	    VSTRING_ADDCH(state->output_buffer, '=');
	    VSTRING_TERMINATE(state->output_buffer);
	    BODY_OUT(state, REC_TYPE_NORM,
		     STR(state->output_buffer),
		     LEN(state->output_buffer));
	    VSTRING_RESET(state->output_buffer);
	}
	/* Append the next character. */
	ch = *cp;
	if ((ch < 32 && ch != '\t') || ch == '=' || ch > 126) {
	    QP_ENCODE(state->output_buffer, ch);
	} else {
	    VSTRING_ADDCH(state->output_buffer, ch);
	}
    }

    /*
     * Flush output after a hard line break (i.e. the end of a REC_TYPE_NORM
     * record). Fix trailing whitespace as per the RFC: in the worst case,
     * the output length will grow from 73 characters to 75 characters.
     */
    if (rec_type == REC_TYPE_NORM) {
	if (LEN(state->output_buffer) > 0
	    && ((ch = END(state->output_buffer)[-1]) == ' ' || ch == '\t')) {
	    vstring_truncate(state->output_buffer,
			     LEN(state->output_buffer) - 1);
	    QP_ENCODE(state->output_buffer, ch);
	}
	VSTRING_TERMINATE(state->output_buffer);
	BODY_OUT(state, REC_TYPE_NORM,
		 STR(state->output_buffer),
		 LEN(state->output_buffer));
	VSTRING_RESET(state->output_buffer);
    }
}
Example #2
0
void mime_state_downgrade(MIME_STATE *state, int rec_type,
	const char *text, ssize_t len)
{
	static char hexchars[] = "0123456789ABCDEF";
	const unsigned char *cp;
	MIME_NODE *node = state->curr_node;
	int     ch;

#define CU_CHAR_PTR(x)	((const unsigned char *) (x))
#define QP_ENCODE(buffer, ch) { \
	buffer += '='; \
	buffer += (char) hexchars[(ch >> 4) & 0xff]; \
	buffer += (char) hexchars[ch & 0xf]; \
}

	/*
	* Insert a soft line break when the output reaches a critical length
	* before we reach a hard line break.
	*/
	for (cp = CU_CHAR_PTR(text); cp < CU_CHAR_PTR(text + len); cp++) {
		/* Critical length before hard line break. */
		if (LEN(node->buffer) > 72) {
			node->buffer += '=';
		}
		/* Append the next character. */
		ch = *cp;
		if ((ch < 32 && ch != '\t') || ch == '=' || ch > 126) {
			QP_ENCODE(node->buffer, ch);
		} else {
			ADDCH(node->buffer, ch);
		}
	}

	/*
	* Flush output after a hard line break (i.e. the end of a REC_TYPE_NORM
	* record). Fix trailing whitespace as per the RFC: in the worst case,
	* the output length will grow from 73 characters to 75 characters.
	*/
	if (rec_type == REC_TYPE_NORM) {
		if (LEN(node->buffer) > 0
			&& ((ch = END(node->buffer)[-1]) == ' ' || ch == '\t'))
		{
			acl_vstring_truncate(node->buffer, LEN(node->buffer) - 1);
			QP_ENCODE(node->buffer, ch);
		}
		ACL_VSTRING_TERMINATE(node->buffer);
	}
}
Example #3
0
ssize_t is_header_buf(const char *str, ssize_t str_len)
{
	const unsigned char *cp;
	int     state;
	int     c;
	ssize_t len;

#define INIT		0
#define IN_CHAR		1
#define IN_CHAR_SPACE	2
#define CU_CHAR_PTR(x)	((const unsigned char *) (x))

	/*
	 * XXX RFC 2822 Section 4.5, Obsolete header fields: whitespace may
	 * appear between header label and ":" (see: RFC 822, Section 3.4.2.).
	 * 
	 * XXX Don't run off the end in case some non-standard iscntrl()
	 * implementation considers null a non-control character...
	 */
	for (len = 0, state = INIT, cp = CU_CHAR_PTR(str); /* see below */; cp++) {
		if (str_len != IS_HEADER_NULL_TERMINATED && str_len-- <= 0)
			return (0);
		switch (c = *cp) {
		default:
			if (c == 0 || !ACL_ISASCII(c) || ACL_ISCNTRL(c))
				return (0);
			if (state == INIT)
				state = IN_CHAR;
			if (state == IN_CHAR) {
				len++;
				continue;
			}
			return (0);
		case ' ':
		case '\t':
			if (state == IN_CHAR)
				state = IN_CHAR_SPACE;
			if (state == IN_CHAR_SPACE)
				continue;
			return (0);
		case ':':
			return ((state == IN_CHAR || state == IN_CHAR_SPACE) ? len : 0);
		}
	}
	/* Redundant return for future proofing. */
	return (0);
}
Example #4
0
int     is_header(const char *str)
{
    const unsigned char *cp;
    int     state;
    int     c;
    int     len;

#define INIT		0
#define IN_CHAR		1
#define IN_CHAR_SPACE	2
#define CU_CHAR_PTR(x)	((const unsigned char *) (x))

    /*
     * XXX RFC 2822 Section 4.5, Obsolete header fields: whitespace may
     * appear between header label and ":" (see: RFC 822, Section 3.4.2.).
     */
    for (len = 0, state = INIT, cp = CU_CHAR_PTR(str); (c = *cp) != 0; cp++) {
        switch (c) {
        default:
            if (!ISASCII(c) || ISCNTRL(c))
                return (0);
            if (state == INIT)
                state = IN_CHAR;
            if (state == IN_CHAR) {
                len++;
                continue;
            }
            return (0);
        case ' ':
        case '\t':
            if (state == IN_CHAR)
                state = IN_CHAR_SPACE;
            if (state == IN_CHAR_SPACE)
                continue;
            return (0);
        case ':':
            return ((state == IN_CHAR || state == IN_CHAR_SPACE) ? len : 0);
        }
    }
    return (0);
}
Example #5
0
ssize_t header_token(HEADER_TOKEN *token, ssize_t token_len,
	ACL_VSTRING *token_buffer, const char **ptr,
	const char *user_specials, int user_terminator)
{
	ssize_t comment_level;
	const unsigned char *cp;
	ssize_t len;
	int     ch;
	ssize_t tok_count;
	ssize_t n;

	/*
	 * Initialize.
	 */
	ACL_VSTRING_RESET(token_buffer);
	cp = CU_CHAR_PTR(*ptr);
	tok_count = 0;
	if (user_specials == 0)
		user_specials = LEX_822_SPECIALS;

	/*
	 * Main parsing loop.
	 * 
	 * XXX What was the reason to continue parsing when user_terminator is
	 * specified? Perhaps this was needed at some intermediate stage of
	 * development?
	 */
	while ((ch = *cp) != 0 && (user_terminator != 0 || tok_count < token_len)) {
		cp++;

		/*
		 * Skip RFC 822 linear white space.
		 */
		if (IS_SPACE_TAB_CR_LF(ch))
			continue;

		/*
		 * Terminator.
		 */
		if (ch == user_terminator)
			break;

		/*
		 * Skip RFC 822 comment.
		 */
		if (ch == '(') {
			comment_level = 1;
			while ((ch = *cp) != 0) {
				cp++;
				if (ch == '(') {  /* comments can nest! */
					comment_level++;
				} else if (ch == ')') {
					if (--comment_level == 0)
						break;
				} else if (ch == '\\') {
					if ((ch = *cp) == 0)
						break;
					cp++;
				}
			}
			continue;
		}

		/*
		 * Copy quoted text according to RFC 822.
		 */
		if (ch == '"') {
			if (tok_count < token_len) {
				token[tok_count].u.offset = LEN(token_buffer);
				token[tok_count].type = HEADER_TOK_QSTRING;
			}
			while ((ch = *cp) != 0) {
				cp++;
				if (ch == '"')
					break;
				if (ch == '\n') {		/* unfold */
					if (tok_count < token_len) {
						len = LEN(token_buffer);
						while (len > 0 && IS_SPACE_TAB_CR_LF(STR(token_buffer)[len - 1]))
							len--;
						if (len < (ssize_t) LEN(token_buffer))
							acl_vstring_truncate(token_buffer, len);
					}
					continue;
				}
				if (ch == '\\') {
					if (tok_count < token_len)
						ACL_VSTRING_ADDCH(token_buffer, ch);

					if (*cp == 0)
						break;
					ch = *cp;
					cp++;
				}
				if (tok_count < token_len)
					ACL_VSTRING_ADDCH(token_buffer, ch);
			}
			if (tok_count < token_len) {
				ACL_VSTRING_ADDCH(token_buffer, 0);
				tok_count++;
			}
			continue;
		}

		/*
		 * Control, or special.
		 */
		if (strchr(user_specials, ch) || ACL_ISCNTRL(ch)) {
			if (tok_count < token_len) {
				token[tok_count].u.offset = LEN(token_buffer);
				token[tok_count].type = ch;
				ACL_VSTRING_ADDCH(token_buffer, ch);
				ACL_VSTRING_ADDCH(token_buffer, 0);
				tok_count++;
			}
			continue;
		}

		/*
		 * Token.
		 */
		else {
			if (tok_count < token_len) {
				token[tok_count].u.offset = LEN(token_buffer);
				token[tok_count].type = HEADER_TOK_TOKEN;
				ACL_VSTRING_ADDCH(token_buffer, ch);
			}
			while ((ch = *cp) != 0 && !IS_SPACE_TAB_CR_LF(ch)
				&& !ACL_ISCNTRL(ch) && !strchr(user_specials, ch)) {
					cp++;
					if (tok_count < token_len)
						ACL_VSTRING_ADDCH(token_buffer, ch);
				}
				if (tok_count < token_len) {
					ACL_VSTRING_ADDCH(token_buffer, 0);
					tok_count++;
				}
				continue;
		}
	}

	/*
	 * Ignore a zero-length item after the last terminator.
	 */
	if (tok_count == 0 && ch == 0)
		return (-1);

	/*
	 * Finalize. Fill in the string pointer array, now that the token buffer
	 * is no longer dynamically reallocated as it grows.
	 */
	*ptr = (const char *) cp;
	for (n = 0; n < tok_count; n++)
		token[n].u.value = STR(token_buffer) + token[n].u.offset;

	//if (acl_msg_verbose)
	//	acl_msg_info("header_token: %s %s %s",
	//		tok_count > 0 ? token[0].u.value : "",
	//		tok_count > 1 ? token[1].u.value : "",
	//		tok_count > 2 ? token[2].u.value : "");

	return (tok_count);
}
Example #6
0
int     mime_state_update(MIME_STATE *state, int rec_type,
			          const char *text, int len)
{
    int     input_is_text = (rec_type == REC_TYPE_NORM
			     || rec_type == REC_TYPE_CONT);
    MIME_STACK *sp;
    HEADER_OPTS *header_info;
    const unsigned char *cp;

#define SAVE_PREV_REC_TYPE_AND_RETURN_ERR_FLAGS(state, rec_type) do { \
	state->prev_rec_type = rec_type; \
	return (state->err_flags); \
    } while (0)

    /*
     * Be sure to flush any partial output line that might still be buffered
     * up before taking any other "end of input" actions.
     */
    if (!input_is_text && state->prev_rec_type == REC_TYPE_CONT)
	mime_state_update(state, REC_TYPE_NORM, "", 0);

    /*
     * This message state machine is kept simple for the sake of robustness.
     * Standards evolve over time, and we want to be able to correctly
     * process messages that are not yet defined. This state machine knows
     * about headers and bodies, understands that multipart/whatever has
     * multiple body parts with a header and body, and that message/whatever
     * has message headers at the start of a body part.
     */
    switch (state->curr_state) {

	/*
	 * First, deal with header information that we have accumulated from
	 * previous input records. Discard text that does not fit in a header
	 * buffer. Our limit is quite generous; Sendmail will refuse mail
	 * with only 32kbyte in all the message headers combined.
	 */
    case MIME_STATE_PRIMARY:
    case MIME_STATE_MULTIPART:
    case MIME_STATE_NESTED:
	if (LEN(state->output_buffer) > 0) {
	    if (input_is_text) {
		if (state->prev_rec_type == REC_TYPE_CONT) {
		    if (LEN(state->output_buffer) < var_header_limit) {
			vstring_strcat(state->output_buffer, text);
		    } else {
			if (state->static_flags & MIME_OPT_REPORT_TRUNC_HEADER)
			    REPORT_ERROR(state, MIME_ERR_TRUNC_HEADER,
					 STR(state->output_buffer));
		    }
		    SAVE_PREV_REC_TYPE_AND_RETURN_ERR_FLAGS(state, rec_type);
		}
		if (IS_SPACE_TAB(*text)) {
		    if (LEN(state->output_buffer) < var_header_limit) {
			vstring_strcat(state->output_buffer, "\n");
			vstring_strcat(state->output_buffer, text);
		    } else {
			if (state->static_flags & MIME_OPT_REPORT_TRUNC_HEADER)
			    REPORT_ERROR(state, MIME_ERR_TRUNC_HEADER,
					 STR(state->output_buffer));
		    }
		    SAVE_PREV_REC_TYPE_AND_RETURN_ERR_FLAGS(state, rec_type);
		}
	    }

	    /*
	     * The input is (the beginning of) another message header, or is
	     * not a message header, or is not even a text record. With no
	     * more input to append to this saved header, do output
	     * processing and reset the saved header buffer. Hold on to the
	     * content transfer encoding header if we have to do a 8->7
	     * transformation, because the proper information depends on the
	     * content type header: message and multipart require a domain,
	     * leaf entities have either a transformation or a domain.
	     */
	    if (LEN(state->output_buffer) > 0) {
		header_info = header_opts_find(STR(state->output_buffer));
		if (!(state->static_flags & MIME_OPT_DISABLE_MIME)
		    && header_info != 0) {
		    if (header_info->type == HDR_CONTENT_TYPE)
			mime_state_content_type(state, header_info);
		    if (header_info->type == HDR_CONTENT_TRANSFER_ENCODING)
			mime_state_content_encoding(state, header_info);
		}
		if ((state->static_flags & MIME_OPT_REPORT_8BIT_IN_HEADER) != 0
		    && (state->err_flags & MIME_ERR_8BIT_IN_HEADER) == 0) {
		    for (cp = CU_CHAR_PTR(STR(state->output_buffer));
			 cp < CU_CHAR_PTR(END(state->output_buffer)); cp++)
			if (*cp & 0200) {
			    REPORT_ERROR(state, MIME_ERR_8BIT_IN_HEADER,
					 STR(state->output_buffer));
			    break;
			}
		}
		/* Output routine is explicitly allowed to change the data. */
		if (header_info == 0
		    || header_info->type != HDR_CONTENT_TRANSFER_ENCODING
		    || (state->static_flags & MIME_OPT_DOWNGRADE) == 0
		    || state->curr_domain == MIME_ENC_7BIT)
		    HEAD_OUT(state, header_info, len);
		state->prev_rec_type = 0;
		VSTRING_RESET(state->output_buffer);
	    }
	}

	/*
	 * With past header information moved out of the way, proceed with a
	 * clean slate.
	 */
	if (input_is_text) {
	    int     header_len;

	    /*
	     * See if this input is (the beginning of) a message header.
	     * Normalize obsolete "name space colon" syntax to "name colon".
	     * Things would be too confusing otherwise.
	     */
	    if ((header_len = is_header(text)) > 0) {
		vstring_strncpy(state->output_buffer, text, header_len);
		for (text += header_len; IS_SPACE_TAB(*text); text++)
		     /* void */ ;
		vstring_strcat(state->output_buffer, text);
		SAVE_PREV_REC_TYPE_AND_RETURN_ERR_FLAGS(state, rec_type);
	    }
	}

	/*
	 * This input terminates a block of message headers. When converting
	 * 8-bit to 7-bit mail, this is the right place to emit the correct
	 * content-transfer-encoding header. With message or multipart we
	 * specify 7bit, with leaf entities we specify quoted-printable.
	 * 
	 * We're not going to convert non-text data into base 64. If they send
	 * arbitrary binary data as 8-bit text, then the data is already
	 * broken beyond recovery, because the Postfix SMTP server sanitizes
	 * record boundaries, treating broken record boundaries as CRLF.
	 * 
	 * Clear the output buffer, we will need it for storage of the
	 * conversion result.
	 */
	if ((state->static_flags & MIME_OPT_DOWNGRADE)
	    && state->curr_domain != MIME_ENC_7BIT) {
	    if (state->curr_ctype == MIME_CTYPE_MESSAGE
		|| state->curr_ctype == MIME_CTYPE_MULTIPART)
		cp = CU_CHAR_PTR("7bit");
	    else
		cp = CU_CHAR_PTR("quoted-printable");
	    vstring_sprintf(state->output_buffer,
			    "Content-Transfer-Encoding: %s", cp);
	    HEAD_OUT(state, (HEADER_OPTS *) 0, len);
	    VSTRING_RESET(state->output_buffer);
	}

	/*
	 * This input terminates a block of message headers. Call the
	 * optional header end routine at the end of the first header block.
	 */
	if (state->curr_state == MIME_STATE_PRIMARY && state->head_end)
	    state->head_end(state->app_context);

	/*
	 * This is the right place to check if the sender specified an
	 * appropriate identity encoding (7bit, 8bit, binary) for multipart
	 * and for message.
	 */
	if (state->static_flags & MIME_OPT_REPORT_ENCODING_DOMAIN) {
	    if (state->curr_ctype == MIME_CTYPE_MESSAGE) {
		if (state->curr_stype == MIME_STYPE_PARTIAL
		    || state->curr_stype == MIME_STYPE_EXTERN_BODY) {
		    if (state->curr_domain != MIME_ENC_7BIT)
			REPORT_ERROR(state, MIME_ERR_ENCODING_DOMAIN,
				 mime_state_enc_name(state->curr_encoding));
		} else {
		    if (state->curr_encoding != state->curr_domain)
			REPORT_ERROR(state, MIME_ERR_ENCODING_DOMAIN,
				 mime_state_enc_name(state->curr_encoding));
		}
	    } else if (state->curr_ctype == MIME_CTYPE_MULTIPART) {
		if (state->curr_encoding != state->curr_domain)
		    REPORT_ERROR(state, MIME_ERR_ENCODING_DOMAIN,
				 mime_state_enc_name(state->curr_encoding));
	    }
	}

	/*
	 * Find out if the next body starts with its own message headers. In
	 * agressive mode, examine headers of partial and external-body
	 * messages. Otherwise, treat such headers as part of the "body". Set
	 * the proper encoding information for the multipart prolog.
	 * 
	 * XXX This changes state to MIME_STATE_NESTED and then outputs a body
	 * line, so that the body offset is not properly reset.
	 */
	if (input_is_text) {
	    if (*text == 0) {
		state->body_offset = 0;		/* XXX */
		if (state->curr_ctype == MIME_CTYPE_MESSAGE) {
		    if (state->curr_stype == MIME_STYPE_RFC822
		    || (state->static_flags & MIME_OPT_RECURSE_ALL_MESSAGE))
			SET_MIME_STATE(state, MIME_STATE_NESTED,
				       MIME_CTYPE_TEXT, MIME_STYPE_PLAIN,
				       MIME_ENC_7BIT, MIME_ENC_7BIT);
		    else
			SET_CURR_STATE(state, MIME_STATE_BODY);
		} else if (state->curr_ctype == MIME_CTYPE_MULTIPART) {
		    SET_MIME_STATE(state, MIME_STATE_BODY,
				   MIME_CTYPE_OTHER, MIME_STYPE_OTHER,
				   MIME_ENC_7BIT, MIME_ENC_7BIT);
		} else {
		    SET_CURR_STATE(state, MIME_STATE_BODY);
		}
	    }

	    /*
	     * Invalid input. Force output of one blank line and jump to the
	     * body state, leaving all other state alone.
	     */
	    else {
		SET_CURR_STATE(state, MIME_STATE_BODY);
		BODY_OUT(state, REC_TYPE_NORM, "", 0);
	    }
	}

	/*
	 * This input is not text. Go to body state, unconditionally.
	 */
	else {
	    SET_CURR_STATE(state, MIME_STATE_BODY);
	}
	/* FALLTHROUGH */

	/*
	 * Body text. Look for message boundaries, and recover from missing
	 * boundary strings. Missing boundaries can happen in agressive mode
	 * with text/rfc822-headers or with message/partial. Ignore non-space
	 * cruft after --boundary or --boundary--, because some MUAs do, and
	 * because only perverse software would take advantage of this to
	 * escape detection. We have to ignore trailing cruft anyway, because
	 * our saved copy of the boundary string may have been truncated for
	 * safety reasons.
	 * 
	 * Optionally look for 8-bit data in content that was announced as, or
	 * that defaults to, 7-bit. Unfortunately, we cannot turn this on by
	 * default. Majordomo sends requests for approval that do not
	 * propagate the MIME information from the enclosed message to the
	 * message headers of the approval request.
	 * 
	 * Set the proper state information after processing a message boundary
	 * string.
	 * 
	 * Don't look for boundary strings at the start of a continued record.
	 */
    case MIME_STATE_BODY:
	if (input_is_text) {
	    if ((state->static_flags & MIME_OPT_REPORT_8BIT_IN_7BIT_BODY) != 0
		&& state->curr_encoding == MIME_ENC_7BIT
		&& (state->err_flags & MIME_ERR_8BIT_IN_7BIT_BODY) == 0) {
		for (cp = CU_CHAR_PTR(text); cp < CU_CHAR_PTR(text + len); cp++)
		    if (*cp & 0200) {
			REPORT_ERROR(state, MIME_ERR_8BIT_IN_7BIT_BODY, text);
			break;
		    }
	    }
	    if (state->stack && state->prev_rec_type != REC_TYPE_CONT
		&& text[0] == '-' && text[1] == '-') {
		for (sp = state->stack; sp != 0; sp = sp->next) {
		    if (strncmp(text + 2, sp->boundary, sp->bound_len) == 0) {
			while (sp != state->stack)
			    mime_state_pop(state);
			if (strncmp(text + 2 + sp->bound_len, "--", 2) == 0) {
			    mime_state_pop(state);
			    SET_MIME_STATE(state, MIME_STATE_BODY,
					 MIME_CTYPE_OTHER, MIME_STYPE_OTHER,
					   MIME_ENC_7BIT, MIME_ENC_7BIT);
			} else {
			    SET_MIME_STATE(state, MIME_STATE_MULTIPART,
					   sp->def_ctype, sp->def_stype,
					   MIME_ENC_7BIT, MIME_ENC_7BIT);
			}
			break;
		    }
		}
	    }
	    /* Put last for consistency with header output routine. */
	    if ((state->static_flags & MIME_OPT_DOWNGRADE)
		&& state->curr_domain != MIME_ENC_7BIT)
		mime_state_downgrade(state, rec_type, text, len);
	    else
		BODY_OUT(state, rec_type, text, len);
	}

	/*
	 * The input is not a text record. Inform the application that this
	 * is the last opportunity to send any pending output.
	 */
	else {
	    if (state->body_end)
		state->body_end(state->app_context);
	}
	SAVE_PREV_REC_TYPE_AND_RETURN_ERR_FLAGS(state, rec_type);

	/*
	 * Oops. This can't happen.
	 */
    default:
	msg_panic("mime_state_update: unknown state: %d", state->curr_state);
    }
}