BOOL
receive_check_fs(int msg_size)
{
-int_eximarith_t space;
int inodes;
if (check_spool_space > 0 || msg_size > 0 || check_spool_inodes > 0)
{
- space = receive_statvfs(TRUE, &inodes);
+ int_eximarith_t space = receive_statvfs(TRUE, &inodes);
DEBUG(D_receive)
debug_printf("spool directory space = " PR_EXIM_ARITH "K inodes = %d "
if (check_log_space > 0 || check_log_inodes > 0)
{
- space = receive_statvfs(FALSE, &inodes);
+ int_eximarith_t space = receive_statvfs(FALSE, &inodes);
DEBUG(D_receive)
debug_printf("log directory space = " PR_EXIM_ARITH "K inodes = %d "
well, so that there are no CRs in spooled messages. However, the message
terminating dot is not recognized between two bare CRs.
+Dec 2023: getting a site to send a body including an "LF . LF" sequence
+followed by SMTP commands is a possible "smtp smuggling" attack. If
+the first (header) line for the message has a proper CRLF then enforce
+that for the body: convert bare LF to a space.
+
Arguments:
- fout a FILE to which to write the message; NULL if skipping
+ fout a FILE to which to write the message; NULL if skipping
+ strict_crlf require full CRLF sequence as a line ending
Returns: One of the END_xxx values indicating why it stopped reading
*/
static int
-read_message_data_smtp(FILE *fout)
+read_message_data_smtp(FILE * fout, BOOL strict_crlf)
{
-int ch_state = 0;
-int ch;
-int linelength = 0;
+enum { s_linestart, s_normal, s_had_cr, s_had_nl_dot, s_had_dot_cr } ch_state =
+ s_linestart;
+int linelength = 0, ch;
while ((ch = (receive_getc)(GETC_BUFFER_UNLIMITED)) != EOF)
{
if (ch == 0) body_zerocount++;
switch (ch_state)
{
- case 0: /* After LF or CRLF */
- if (ch == '.')
- {
- ch_state = 3;
- continue; /* Don't ever write . after LF */
- }
- ch_state = 1;
+ case s_linestart: /* After LF or CRLF */
+ if (ch == '.')
+ {
+ ch_state = s_had_nl_dot;
+ continue; /* Don't ever write . after LF */
+ }
+ ch_state = s_normal;
- /* Else fall through to handle as normal uschar. */
+ /* Else fall through to handle as normal uschar. */
- case 1: /* Normal state */
- if (ch == '\n')
- {
- ch_state = 0;
- body_linecount++;
+ case s_normal: /* Normal state */
+ if (ch == '\r')
+ {
+ ch_state = s_had_cr;
+ continue; /* Don't write the CR */
+ }
+ if (ch == '\n') /* Bare LF at end of line */
+ if (strict_crlf)
+ ch = ' '; /* replace LF with space */
+ else
+ { /* treat as line ending */
+ ch_state = s_linestart;
+ body_linecount++;
+ if (linelength > max_received_linelength)
+ max_received_linelength = linelength;
+ linelength = -1;
+ }
+ break;
+
+ case s_had_cr: /* After (unwritten) CR */
+ body_linecount++; /* Any char ends line */
if (linelength > max_received_linelength)
- max_received_linelength = linelength;
+ max_received_linelength = linelength;
linelength = -1;
- }
- else if (ch == '\r')
- {
- ch_state = 2;
- continue;
- }
- break;
+ if (ch == '\n') /* proper CRLF */
+ ch_state = s_linestart;
+ else
+ {
+ message_size++; /* convert the dropped CR to a stored NL */
+ if (fout && fputc('\n', fout) == EOF) return END_WERROR;
+ cutthrough_data_put_nl();
+ if (ch == '\r') /* CR; do not write */
+ continue;
+ ch_state = s_normal; /* not LF or CR; process as standard */
+ }
+ break;
- case 2: /* After (unwritten) CR */
- body_linecount++;
- if (linelength > max_received_linelength)
- max_received_linelength = linelength;
- linelength = -1;
- if (ch == '\n')
- {
- ch_state = 0;
- }
- else
- {
- message_size++;
- if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
- cutthrough_data_put_nl();
- if (ch != '\r') ch_state = 1; else continue;
- }
- break;
+ case s_had_nl_dot: /* After [CR] LF . */
+ if (ch == '\n') /* [CR] LF . LF */
+ if (strict_crlf)
+ ch = ' '; /* replace LF with space */
+ else
+ return END_DOT;
+ else if (ch == '\r') /* [CR] LF . CR */
+ {
+ ch_state = s_had_dot_cr;
+ continue; /* Don't write the CR */
+ }
+ /* The dot was removed on reaching s_had_nl_dot. For a doubled dot, here,
+ reinstate it to cutthrough. The current ch, dot or not, is passed both to
+ cutthrough and to file below. */
+ else if (ch == '.')
+ {
+ uschar c = ch;
+ cutthrough_data_puts(&c, 1);
+ }
+ ch_state = s_normal;
+ break;
- case 3: /* After [CR] LF . */
- if (ch == '\n')
- return END_DOT;
- if (ch == '\r')
- {
- ch_state = 4;
- continue;
- }
- /* The dot was removed at state 3. For a doubled dot, here, reinstate
- it to cutthrough. The current ch, dot or not, is passed both to cutthrough
- and to file below. */
- if (ch == '.')
- {
- uschar c= ch;
- cutthrough_data_puts(&c, 1);
- }
- ch_state = 1;
- break;
+ case s_had_dot_cr: /* After [CR] LF . CR */
+ if (ch == '\n')
+ return END_DOT; /* Preferred termination */
- case 4: /* After [CR] LF . CR */
- if (ch == '\n') return END_DOT;
- message_size++;
- body_linecount++;
- if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
- cutthrough_data_put_nl();
- if (ch == '\r')
- {
- ch_state = 2;
- continue;
- }
- ch_state = 1;
- break;
+ message_size++; /* convert the dropped CR to a stored NL */
+ body_linecount++;
+ if (fout && fputc('\n', fout) == EOF) return END_WERROR;
+ cutthrough_data_put_nl();
+ if (ch == '\r')
+ {
+ ch_state = s_had_cr;
+ continue; /* CR; do not write */
+ }
+ ch_state = s_normal;
+ break;
}
/* Add the character to the spool file, unless skipping; then loop for the
{
if (message_ended >= END_NOTENDED)
message_ended = chunking_state <= CHUNKING_OFFERED
- ? read_message_data_smtp(NULL)
+ ? read_message_data_smtp(NULL, FALSE)
: read_message_bdat_smtp_wire(NULL);
}
give_local_error(int errcode, uschar *text1, uschar *text2, int error_rc,
FILE *f, header_line *hptr)
{
+DEBUG(D_all) debug_printf("%s%s\n", text2, text1);
+
if (error_handling == ERRORS_SENDER)
{
error_block eblock;
header_line * received_header= header_list;
if (recipients_count == 1) received_for = recipients_list[0].address;
+GET_OPTION("received_header_text");
received = expand_string(received_header_text);
received_for = NULL;
/* Working header pointers */
rmark reset_point;
-header_line *next;
+header_line * next;
/* Flags for noting the existence of certain headers (only one left) */
/* Pointers to receive the addresses of headers whose contents we need. */
-header_line *from_header = NULL;
-header_line *subject_header = NULL;
-header_line *msgid_header = NULL;
-header_line *received_header;
+header_line * from_header = NULL;
+#ifdef SUPPORT_DMARC
+header_line * dmarc_from_header = NULL;
+#endif
+header_line * subject_header = NULL, * msgid_header = NULL, * received_header;
BOOL msgid_header_newly_created = FALSE;
/* Variables for use when building the Received: header. */
-uschar *timestamp;
+uschar * timestamp;
int tslen;
/* Time of creation of message_id */
if (ch == '\n')
{
- if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = FALSE;
- else if (first_line_ended_crlf) receive_ungetc(' ');
+ if (first_line_ended_crlf == TRUE_UNSET)
+ first_line_ended_crlf = FALSE;
+ else if (first_line_ended_crlf)
+ receive_ungetc(' ');
goto EOL;
}
This implements the dot-doubling rule, though header lines starting with
dots aren't exactly common. They are legal in RFC 822, though. If the
following is CRLF or LF, this is the line that that terminates the
+
entire message. We set message_ended to indicate this has happened (to
prevent further reading), and break out of the loop, having freed the
empty header, and set next = NULL to indicate no data line. */
if (f.dot_ends && ptr == 0 && ch == '.')
{
+ /* leading dot while in headers-read mode */
ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
- if (ch == '\r')
+ if (ch == '\n' && first_line_ended_crlf == TRUE /* and not TRUE_UNSET */ )
+ /* dot, LF but we are in CRLF mode. Attack? */
+ ch = ' '; /* replace the LF with a space */
+
+ else if (ch == '\r')
{
ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
if (ch != '\n')
ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
if (ch == '\n')
{
- if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = TRUE;
+ if (first_line_ended_crlf == TRUE_UNSET)
+ first_line_ended_crlf = TRUE;
goto EOL;
}
{
if (!f.sender_address_forced)
{
- uschar *uucp_sender = expand_string(uucp_from_sender);
- if (!uucp_sender)
+ uschar * uucp_sender;
+ GET_OPTION("uucp_from_sender");
+ if (!(uucp_sender = expand_string(uucp_from_sender)))
log_write(0, LOG_MAIN|LOG_PANIC,
"expansion of \"%s\" failed after matching "
"\"From \" line: %s", uucp_from_sender, expand_string_message);
if (isspace(*p)) break;
while (mac_isgraph(*p) && *p != ':') p++;
- while (isspace(*p)) p++;
- if (*p != ':')
+ if (Uskip_whitespace(&p) != ':')
{
body_zerocount = had_zero;
break;
case htype_from:
h->type = htype_from;
+#ifdef SUPPORT_DMARC
+ if (!is_resent) dmarc_from_header = h;
+#endif
if (!resents_exist || is_resent)
{
from_header = h;
if (!smtp_input)
{
int len;
- uschar *s = Ustrchr(h->text, ':') + 1;
- while (isspace(*s)) s++;
+ uschar * s = Ustrchr(h->text, ':') + 1;
+
+ Uskip_whitespace(&s);
len = h->slen - (s - h->text) - 1;
if (Ustrlen(originator_login) == len &&
strncmpic(s, originator_login, len) == 0)
{
- uschar *name = is_resent? US"Resent-From" : US"From";
+ uschar * name = is_resent ? US"Resent-From" : US"From";
header_add(htype_from, "%s: %s <%s@%s>\n", name, originator_name,
originator_login, qualify_domain_sender);
from_header = header_last;
if (filter_test != FTEST_NONE)
{
- uschar *start = h->text + 12;
- uschar *end = start + Ustrlen(start);
- while (isspace(*start)) start++;
+ uschar * start = h->text + 12;
+ uschar * end = start + Ustrlen(start);
+
+ Uskip_whitespace(&start);
while (end > start && isspace(end[-1])) end--;
if (*start == '<' && end[-1] == '>')
- {
- start++;
- end--;
- }
+ { start++; end--; }
return_path = string_copyn(start, end - start);
printf("Return-path taken from \"Return-path:\" header line\n");
}
if ((h->type == htype_to || h->type == htype_cc || h->type == htype_bcc) &&
(!contains_resent_headers || strncmpic(h->text, US"resent-", 7) == 0))
{
- uschar *s = Ustrchr(h->text, ':') + 1;
- while (isspace(*s)) s++;
+ uschar * s = Ustrchr(h->text, ':') + 1;
+ Uskip_whitespace(&s);
f.parse_allow_group = TRUE; /* Allow address group syntax */
- while (*s != 0)
+ while (*s)
{
uschar *ss = parse_find_address_end(s, FALSE);
uschar *recipient, *errmess, *pp;
/* Check on maximum */
- if (recipients_max > 0 && ++rcount > recipients_max)
+ if (recipients_max_expanded > 0 && ++rcount > recipients_max_expanded)
give_local_error(ERRMESS_TOOMANYRECIP, US"too many recipients",
US"message rejected: ", error_rc, stdin, NULL);
/* Does not return */
/* Move on past this address */
s = ss + (*ss ? 1 : 0);
- while (isspace(*s)) s++;
+ Uskip_whitespace(&s);
} /* Next address */
f.parse_allow_group = FALSE; /* Reset group syntax flags */
/* Permit only letters, digits, dots, and hyphens in the domain */
+ GET_OPTION("message_id_header_domain");
if (message_id_domain)
{
uschar *new_id_domain = expand_string(message_id_domain);
/* Permit all characters except controls and RFC 2822 specials in the
additional text part. */
+ GET_OPTION("message_id_header_text");
if (message_id_text)
{
uschar *new_id_text = expand_string(message_id_text);
it has already been rewritten as part of verification for SMTP input. */
DEBUG(D_rewrite)
- { debug_printf("global rewrite rules\n"); acl_level++; }
+ { debug_printf("rewrite rules on sender address\n"); acl_level++; }
if (global_rewrite_rules && !sender_address_unrewritten && *sender_address)
{
/* deconst ok as src was not const */
by the -t command line option. An added Sender: gets rewritten here. */
DEBUG(D_rewrite)
- { debug_printf("rewrite headers\n"); acl_level++; }
+ { debug_printf("qualify and rewrite headers\n"); acl_level++; }
for (header_line * h = header_list->next, * newh; h; h = h->next)
if ((newh = rewrite_header(h, NULL, NULL, global_rewrite_rules,
rewrite_existflags, TRUE)))
DEBUG(D_receive)
{
debug_printf(">>Headers after rewriting and local additions:\n");
+ acl_level++;
for (header_line * h = header_list->next; h; h = h->next)
- debug_printf("%c %s", h->type, h->text);
+ debug_printf_indent("%c %s", h->type, h->text);
debug_printf("\n");
+ acl_level--;
}
/* The headers are now complete in store. If we are running in filter
/* Open a new spool file for the data portion of the message. We need
-to access it both via a file descriptor and a stream. Try to make the
+to access it both via a file descriptor and a stdio stream. Try to make the
directory if it isn't there. */
spool_name = spool_fname(US"input", message_subdir, message_id, US"-D");
if (smtp_input)
{
message_ended = chunking_state <= CHUNKING_OFFERED
- ? read_message_data_smtp(spool_data_file)
+ ? read_message_data_smtp(spool_data_file, first_line_ended_crlf)
: spool_wireformat
? read_message_bdat_smtp_wire(spool_data_file)
: read_message_bdat_smtp(spool_data_file);
dkim_exim_verify_finish();
/* Check if we must run the DKIM ACL */
+ GET_OPTION("acl_smtp_dkim");
if (acl_smtp_dkim && dkim_verify_signers && *dkim_verify_signers)
{
uschar * dkim_verify_signers_expanded =
#endif /* DISABLE_DKIM */
#ifdef WITH_CONTENT_SCAN
- if ( recipients_count > 0
- && acl_smtp_mime
- && !run_mime_acl(acl_smtp_mime, &smtp_yield, &smtp_reply, &blackholed_by)
- )
- goto TIDYUP;
+ if (recipients_count > 0)
+ {
+ GET_OPTION("acl_smtp_mime");
+ if (acl_smtp_mime
+ && !run_mime_acl(acl_smtp_mime, &smtp_yield, &smtp_reply, &blackholed_by)
+ )
+ goto TIDYUP;
+ }
#endif /* WITH_CONTENT_SCAN */
#ifdef SUPPORT_DMARC
- dmarc_store_data(from_header);
+ dmarc_store_data(dmarc_from_header);
#endif
#ifndef DISABLE_PRDR
- if (prdr_requested && recipients_count > 1 && acl_smtp_data_prdr)
+ if (prdr_requested && recipients_count > 1)
{
- int all_pass = OK;
- int all_fail = FAIL;
+ GET_OPTION("acl_smtp_data_prdr");
+ if (acl_smtp_data_prdr)
+ {
+ int all_pass = OK;
+ int all_fail = FAIL;
- smtp_printf("353 PRDR content analysis beginning\r\n", SP_MORE);
- /* Loop through recipients, responses must be in same order received */
- for (unsigned int c = 0; recipients_count > c; c++)
- {
- const uschar * addr = recipients_list[c].address;
- uschar * msg= US"PRDR R=<%s> %s";
- uschar * code;
- DEBUG(D_receive)
- debug_printf("PRDR processing recipient %s (%d of %d)\n",
- addr, c+1, recipients_count);
- rc = acl_check(ACL_WHERE_PRDR, addr,
- acl_smtp_data_prdr, &user_msg, &log_msg);
-
- /* If any recipient rejected content, indicate it in final message */
- all_pass |= rc;
- /* If all recipients rejected, indicate in final message */
- all_fail &= rc;
-
- switch (rc)
- {
- case OK: case DISCARD: code = US"250"; break;
- case DEFER: code = US"450"; break;
- default: code = US"550"; break;
- }
- if (user_msg != NULL)
- smtp_user_msg(code, user_msg);
- else
+ smtp_printf("353 PRDR content analysis beginning\r\n", SP_MORE);
+ /* Loop through recipients, responses must be in same order received */
+ for (unsigned int c = 0; recipients_count > c; c++)
{
+ const uschar * addr = recipients_list[c].address;
+ uschar * msg= US"PRDR R=<%s> %s";
+ uschar * code;
+ DEBUG(D_receive)
+ debug_printf("PRDR processing recipient %s (%d of %d)\n",
+ addr, c+1, recipients_count);
+ rc = acl_check(ACL_WHERE_PRDR, addr,
+ acl_smtp_data_prdr, &user_msg, &log_msg);
+
+ /* If any recipient rejected content, indicate it in final message */
+ all_pass |= rc;
+ /* If all recipients rejected, indicate in final message */
+ all_fail &= rc;
+
switch (rc)
- {
- case OK: case DISCARD:
- msg = string_sprintf(CS msg, addr, "acceptance"); break;
- case DEFER:
- msg = string_sprintf(CS msg, addr, "temporary refusal"); break;
- default:
- msg = string_sprintf(CS msg, addr, "refusal"); break;
- }
- smtp_user_msg(code, msg);
- }
- if (log_msg) log_write(0, LOG_MAIN, "PRDR %s %s", addr, log_msg);
- else if (user_msg) log_write(0, LOG_MAIN, "PRDR %s %s", addr, user_msg);
- else log_write(0, LOG_MAIN, "%s", CS msg);
+ {
+ case OK: case DISCARD: code = US"250"; break;
+ case DEFER: code = US"450"; break;
+ default: code = US"550"; break;
+ }
+ if (user_msg != NULL)
+ smtp_user_msg(code, user_msg);
+ else
+ {
+ switch (rc)
+ {
+ case OK: case DISCARD:
+ msg = string_sprintf(CS msg, addr, "acceptance"); break;
+ case DEFER:
+ msg = string_sprintf(CS msg, addr, "temporary refusal"); break;
+ default:
+ msg = string_sprintf(CS msg, addr, "refusal"); break;
+ }
+ smtp_user_msg(code, msg);
+ }
+ if (log_msg) log_write(0, LOG_MAIN, "PRDR %s %s", addr, log_msg);
+ else if (user_msg) log_write(0, LOG_MAIN, "PRDR %s %s", addr, user_msg);
+ else log_write(0, LOG_MAIN, "%s", CS msg);
- if (rc != OK) { receive_remove_recipient(addr); c--; }
- }
- /* Set up final message, used if data acl gives OK */
- smtp_reply = string_sprintf("%s id=%s message %s",
- all_fail == FAIL ? US"550" : US"250",
- message_id,
- all_fail == FAIL
- ? US"rejected for all recipients"
- : all_pass == OK
- ? US"accepted"
- : US"accepted for some recipients");
- if (recipients_count == 0)
- goto NOT_ACCEPTED;
+ if (rc != OK) { receive_remove_recipient(addr); c--; }
+ }
+ /* Set up final message, used if data acl gives OK */
+ smtp_reply = string_sprintf("%s id=%s message %s",
+ all_fail == FAIL ? US"550" : US"250",
+ message_id,
+ all_fail == FAIL
+ ? US"rejected for all recipients"
+ : all_pass == OK
+ ? US"accepted"
+ : US"accepted for some recipients");
+ if (recipients_count == 0)
+ goto NOT_ACCEPTED;
+ }
+ else
+ prdr_requested = FALSE;
}
else
prdr_requested = FALSE;
/* Check the recipients count again, as the MIME ACL might have changed
them. */
+ GET_OPTION("acl_smtp_data");
if (acl_smtp_data && recipients_count > 0)
{
rc = acl_check(ACL_WHERE_DATA, NULL, acl_smtp_data, &user_msg, &log_msg);
{
#ifdef WITH_CONTENT_SCAN
+ GET_OPTION("acl_not_smtp_mime");
if ( acl_not_smtp_mime
&& !run_mime_acl(acl_not_smtp_mime, &smtp_yield, &smtp_reply,
&blackholed_by)
goto TIDYUP;
#endif /* WITH_CONTENT_SCAN */
+ GET_OPTION("acl_not_smtp");
if (acl_not_smtp)
{
- uschar *user_msg, *log_msg;
+ uschar * user_msg, * log_msg;
f.authentication_local = TRUE;
rc = acl_check(ACL_WHERE_NOTSMTP, NULL, acl_not_smtp, &user_msg, &log_msg);
if (rc == DISCARD)
receive_messagecount++;
-if (fflush(spool_data_file))
+if ( fflush(spool_data_file)
+#if _POSIX_C_SOURCE >= 199309L || _XOPEN_SOURCE >= 500
+# ifdef ENABLE_DISABLE_FSYNC
+ || !disable_fsync && fdatasync(data_fd)
+# else
+ || fdatasync(data_fd)
+# endif
+#endif
+ )
{
errmsg = string_sprintf("Spool write error: %s", strerror(errno));
log_write(0, LOG_MAIN, "%s\n", errmsg);
possible for fclose() to fail - and this has been seen on obscure filesystems
(probably one that delayed the actual media write as long as possible)
but what to do? What has happened to the lock if this happens?
-It's a mes because we already logged the acceptance.
+It's a mess because we already logged the acceptance.
We can at least log the issue, try to remove spoolfiles and respond with
a temp-reject. We do not want to close before logging acceptance because
we want to hold the lock until we know that logging worked.
}
/* End of receive.c */
+/* vi: se aw ai sw=2
+*/