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); } }
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); } }
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); }
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); }
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); }
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); } }