Patrik Rak's clever queue manager scheduler (nqmgr). Files:
conf/sample-scheduler.cf, README_FILES/SCHEDULER_README.
+20030429
+
+ Bugfix: while verifying an address, the LMTP client entered
+ a forbidden "next" sender state after the last recipient.
+ Fix by Vladimir Davydoff. File: lmtp/lmtp_proto.c.
+
+ Bugfix: proxymap server did not parse "," as space.
+ Leandro Santi. File: proxymap/proxymap.c.
+
+20030502
+
+ Bugfix: defer delivery after .forward etc. file read error.
+ File: local/token.c.
+
+20030503
+
+ Bugfix: the Postfix LMTP client used the wrong service
+ name, causing trouble with SASL 2.1.13. Daniel Schales,
+ Louisiana Tech. File: lmtp/lmtp_sasl_glue.c.
+
+20030518
+
+ Workaround: IRIX select() reports that a non-blocking file
+ descriptor is writable while write() transfers zero bytes.
+ File: util/vstream.c.
+
+20030519
+
+ Feature: new require_{date,from,message_id,received}_header
+ restriction to reject SMTP mail when some message header
+ is missing. Only the From: and Date: headers are actually
+ required by Internet mail standards; the Received: and
+ Message-ID: headers are optional, but these are often
+ missing from SPAM email. Files: global/cleanup_user.h,
+ global/cleanup_strerror.c, smtpd/smtpd_check.c,
+ cleanup/cleanup_message.c.
+
Open problems:
Low: smtp-source may block when sending large test messages.
meant to become extensible:
-------------------------------------------------------------------
-Class Description
+Class Description
-------------------------------------------------------------------
-local For UNIX accounts and for traditional /etc/aliases
- Domain names are listed in $mydestination (or match the IP
- address listed with $inet_interfaces)
- Known recipients are listed in $local_recipient_maps (this
- information is currently used by the Postfix SMTP server
- only; if $local_recipient_maps is empty, the Postfix
- SMTP server accepts all recipients)
- Default delivery agent: local
+local For UNIX accounts and for traditional /etc/aliases
+ Domain names are listed in $mydestination (or match the IP
+ address listed with $inet_interfaces)
+ Known recipients are listed in $local_recipient_maps (this
+ information is currently used by the Postfix SMTP server
+ only; if $local_recipient_maps is empty, the Postfix
+ SMTP server accepts all recipients)
+ Default delivery agent: local
virtual For hosted domains that are aliased to mailboxes in other
-alias domains
- Known recipients are listed in $virtual_alias_maps (default
- is $virtual_maps for Postfix 1.1 compatibility)
- Domain names are listed in $virtual_alias_domains (default
- is $virtual_alias_maps for Postfix 1.1 compatibility)
-
-virtual For hosted domains with their own mailboxes
-mailbox Known recipients are listed in $virtual_mailbox_maps (if
- this parameter is empty, the Postfix SMTP server accepts
- all recipients for domains listed in $virtual_mailbox_domains)
- Domain names are listed in $virtual_mailbox_domains (default
- is $virtual_mailbox_maps for Postfix 1.1 compatibility)
- Default delivery agent: virtual
-
-relay For remote destinations that list your system as MX host
- Domain names are listed in $relay_domains
- Known recipients are listed in $relay_recipient_maps (if
- this parameter is empty, the Postfix SMTP server accepts
- all recipients for domains listed in $relay_domains)
- Default delivery agent: relay (clone of default smtp agent)
-
-other Restricted to mail from authorized clients
- Default delivery agent: smtp
- No domain table
- No recipient table
+alias domains
+ Known recipients are listed in $virtual_alias_maps (default
+ is $virtual_maps for Postfix 1.1 compatibility)
+ Domain names are listed in $virtual_alias_domains (default
+ is $virtual_alias_maps for Postfix 1.1 compatibility)
+
+virtual For hosted domains with their own mailboxes
+mailbox Known recipients are listed in $virtual_mailbox_maps (if
+ this parameter is empty, the Postfix SMTP server accepts
+ all recipients for domains listed in $virtual_mailbox_domains)
+ Domain names are listed in $virtual_mailbox_domains (default
+ is $virtual_mailbox_maps for Postfix 1.1 compatibility)
+ Default delivery agent: virtual
+
+relay For remote destinations that list your system as MX host
+ Domain names are listed in $relay_domains
+ Known recipients are listed in $relay_recipient_maps (if
+ this parameter is empty, the Postfix SMTP server accepts
+ all recipients for domains listed in $relay_domains)
+ Default delivery agent: relay (clone of default smtp agent)
+
+other Restricted to mail from authorized clients
+ Default delivery agent: smtp
+ No domain table
+ No recipient table
-------------------------------------------------------------------
Incompatibilities with Postfix 1.1
- The local_recipient_maps feature is now turned on by default, so
that the Postfix SMTP server rejects mail for unknown local
- recipients. This is enabled by default. See the LOCAL_RECIPIENT_README
- file hints and tips.
+ recipients. See the LOCAL_RECIPIENT_README file hints and tips.
- Introduction of relay delivery transport in master.cf. This helps
to avoid mail delivery scheduling problems on inbound mail relays,
Note: the localhost port 10025 SMTP server filter should announce
itself as "220 localhost...". Postfix aborts delivery when it
-connects to an SMTP server that uses the same hostname, because
-that normally means you have a mail delivery loop problem.
+connects to an SMTP server that uses the same hostname as Postfix
+("host <servername> greeted me with my own hostname"), because that
+normally means you have a mail delivery loop problem.
The example here assumes that the /some/where/filter command is a
PERL script. PERL has modules that make talking SMTP easy. The
The "-o local_recipient_maps=" and "-o relay_recipient_maps=" avoid
unnecessary table lookups.
-The "-o myhostname=localhost.domain.tld" avoids a possible problem
-if your content filter is based on a proxy that simply relays SMTP
-commands.
+The "-o myhostname=localhost.domain.tld" avoids false alarms ("host
+<servername> greeted me with my own hostname") if your content
+filter is based on a proxy that simply relays SMTP commands.
The "-o smtpd_xxx_restrictions" and "-o mynetworks=127.0.0.0/8"
turn off UCE controls that would only waste time here.
choices in its design.
1 - Round-robin selection by destination for mail that is delivered
- via the same message delivery transport. That strategy broke
- down when one single destination (say, inbound mail) had to
- compete with multiple other destinations (say, outbound mail).
- The poor suffering destination would be selected only
+ via the same message delivery transport. The round-robin strategy
+ was chosen with the intention to prevent a single (destination)
+ site from using up too many mail delivery resources. However,
+ that strategy penalized inbound mail on bi-directional gateways.
+ The poor suffering inbound destination would be selected only
1/number-of-destinations of the time, even when it had more
mail than other destinations, and thus mail could be delayed.
more to add to the message scheduling idea itself. There are few
things which make it look more complicated than it is, but the
algorithm is the same as the user percieves it. The summary of the
-changes from the user's view:
+differences of the programmer's view from the user's view are:
1) Simplification of terms for users: The user knows about messages
and recipients. The program itself works with jobs (one message is
Specifies the list of domains that should be delivered to the
$virtual_transport delivery agent (default: virtual). As of
- version 1.2, Postfix is smart enough that you don't have to
+ version 2.0, Postfix is smart enough that you don't have to
list every virtual domain in a Postfix transport map.
virtual_mailbox_maps
#
-# Postfix master process configuration file. Each line describes how
-# a mailer component program should be run. The fields that make up
-# each line are described below. A "-" field value requests that a
-# default value be used for that field.
+# Postfix master process configuration file. Each logical line
+# describes how a Postfix daemon program should be run.
+#
+# A logical line starts with non-whitespace, non-comment text.
+# Empty lines and whitespace-only lines are ignored, as are comment
+# lines whose first non-whitespace character is a `#'.
+# A line that starts with whitespace continues a logical line.
+#
+# The fields that make up each line are described below. A "-" field
+# value requests that a default value be used for that field.
#
# Service: any name that is valid for the specified transport type
# (the next field). With INET transports, a service is specified as
# SPECIFY ONLY PROGRAMS THAT ARE WRITTEN TO RUN AS POSTFIX DAEMONS.
# ALL DAEMONS SPECIFIED HERE MUST SPEAK A POSTFIX-INTERNAL PROTOCOL.
#
-# DO NOT CHANGE THE ZERO PROCESS LIMIT FOR CLEANUP/BOUNCE/DEFER OR
-# POSTFIX WILL BECOME STUCK UP UNDER HEAVY LOAD
-#
-# DO NOT CHANGE THE ONE PROCESS LIMIT FOR PICKUP/QMGR OR POSTFIX WILL
-# DELIVER MAIL MULTIPLE TIMES.
-#
# DO NOT SHARE THE POSTFIX QUEUE BETWEEN MULTIPLE POSTFIX INSTANCES.
#
# ==========================================================================
#
# EXAMPLE SMTPD ACCESS MAP
# # Protect your outgoing majordomo exploders
-# /^(?!owner-)(.*)-outgoing@/ 550 Use ${1}@${2} instead
+# /^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
#
# # Bounce friend@whatever, except when whatever is our domain (you would
# # be better just bouncing all friend@ mail - this is just an example).
#
# This blocks mail from poorly written mail software.
#
-strict_mime_domain_encoding = no
+strict_mime_encoding_domain = no
# Protect your outgoing majordomo exploders
#
-/^(?!owner-)(.*)-outgoing@/ 550 Use ${1}@${2} instead
+/^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
# Bounce friend@whatever, except when whatever is our domain (you would
/^postmaster@/ OK
# Protect your outgoing majordomo exploders
-/^(.*)-outgoing@(.*)$/!/^owner-.*/ 550 Use ${1}@${2} instead
+if !/^owner-.*/
+/^(.*)-outgoing@(.*)$/ 550 Use ${1}@${2} instead
+endif
<b>EXAMPLE</b> <b>SMTPD</b> <b>ACCESS</b> <b>MAP</b>
# Protect your outgoing majordomo exploders
- /^(?!owner-)(.*)-outgoing@/ 550 Use ${1}@${2} instead
+ /^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
# Bounce friend@whatever, except when whatever is our domain (you would
# be better just bouncing all friend@ mail - this is just an example).
.na
.nf
# Protect your outgoing majordomo exploders
-/^(?!owner-)(.*)-outgoing@/ 550 Use ${1}@${2} instead
+/^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
# Bounce friend@whatever, except when whatever is our domain (you would
# be better just bouncing all friend@ mail - this is just an example).
request contains valid 8-bit MIME mail, and it breaks bounces from
mailers that do not properly encapsulate 8-bit content (for example,
bounces from qmail or from old versions of Postfix).
-.IP \fBstrict_mime_domain_encoding\fR
+.IP \fBstrict_mime_encoding_domain\fR
Reject mail with invalid \fBContent-Transfer-Encoding:\fR
information for message/* or multipart/*. This blocks mail
from poorly written software.
# or $(n) if they aren't followed by whitespace.
# EXAMPLE SMTPD ACCESS MAP
# # Protect your outgoing majordomo exploders
-# /^(?!owner-)(.*)-outgoing@/ 550 Use ${1}@${2} instead
+# /^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
#
# # Bounce friend@whatever, except when whatever is our domain (you would
# # be better just bouncing all friend@ mail - this is just an example).
* our status report.
*/
if (CLEANUP_OUT_OK(state) == 0 && type > 0) {
- if ((state->errs & CLEANUP_STAT_CONT) == 0
- && (state->flags & CLEANUP_FLAG_DISCARD) == 0)
- msg_warn("%s: skipping further client input", state->queue_id);
while (type != REC_TYPE_END
&& (type = rec_get(src, buf, 0)) > 0)
/* void */ ;
return;
}
if (type == REC_TYPE_FLGS) {
+ if (msg_verbose)
+ msg_info("envelope %c %.*s", type, len, buf);
extra_flags = atol(buf);
if (extra_flags & ~CLEANUP_FLAG_MASK_EXTRA)
msg_warn("%s: bad extra flags: 0x%x", state->queue_id, extra_flags);
}
}
+/* cleanup_missing - handle missing message header */
+
+static void cleanup_missing(CLEANUP_STATE *state, const char *resent,
+ const char *header)
+{
+ const char *attr;
+
+ if ((attr = nvtable_find(state->attr, MAIL_ATTR_ORIGIN)) == 0)
+ attr = "unknown";
+ vstring_sprintf(state->temp1, "%s: reject: missing %s%s header from %s;",
+ state->queue_id, resent, header, attr);
+ if (state->sender)
+ vstring_sprintf_append(state->temp1, " from=<%s>", state->sender);
+ if (state->recip)
+ vstring_sprintf_append(state->temp1, " to=<%s>", state->recip);
+ if ((attr = nvtable_find(state->attr, MAIL_ATTR_PROTO_NAME)) != 0)
+ vstring_sprintf_append(state->temp1, " proto=%s", attr);
+ if ((attr = nvtable_find(state->attr, MAIL_ATTR_HELO_NAME)) != 0)
+ vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
+ msg_info("%s", vstring_str(state->temp1));
+ state->errs |= CLEANUP_STAT_MISS_HDR;
+}
+
/* cleanup_header_done_callback - insert missing message headers */
static void cleanup_header_done_callback(void *context)
struct tm *tp;
TOK822 *token;
+ /*
+ * Postfix prepends a Received: message header, so we should see two when
+ * one is required.
+ */
+ if ((state->flags & CLEANUP_FLAG_NEED_RCVD) && state->hop_count < 2) {
+ cleanup_missing(state, "", "Received");
+ return;
+ }
+
/*
* Add a missing (Resent-)Message-Id: header. The message ID gives the
* time in GMT units, plus the local queue ID.
*/
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_MESSAGE_ID : HDR_MESSAGE_ID))) == 0) {
+ if (state->flags & CLEANUP_FLAG_NEED_MSGID) {
+ cleanup_missing(state, state->resent, "Message-Id");
+ return;
+ }
tp = gmtime(&state->time);
strftime(time_stamp, sizeof(time_stamp), "%Y%m%d%H%M%S", tp);
cleanup_out_format(state, REC_TYPE_NORM, "%sMessage-Id: <%s.%s@%s>",
*/
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_DATE : HDR_DATE))) == 0) {
+ if (state->flags & CLEANUP_FLAG_NEED_DATE) {
+ cleanup_missing(state, state->resent, "Date");
+ return;
+ }
cleanup_out_format(state, REC_TYPE_NORM, "%sDate: %s",
state->resent, mail_date(state->time));
}
*/
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_FROM : HDR_FROM))) == 0) {
+ if (state->flags & CLEANUP_FLAG_NEED_FROM) {
+ cleanup_missing(state, state->resent, "From");
+ return;
+ }
quote_822_local(state->temp1, *state->sender ?
state->sender : MAIL_ADDR_MAIL_DAEMON);
vstring_sprintf(state->temp2, "%sFrom: %s",
CLEANUP_STAT_BAD, "Internal protocol error",
CLEANUP_STAT_RCPT, "No recipients specified",
CLEANUP_STAT_HOPS, "Too many hops",
+ CLEANUP_STAT_MISS_HDR, "Missing message header",
CLEANUP_STAT_SIZE, "Message file too big",
CLEANUP_STAT_CONT, "Message content rejected",
CLEANUP_STAT_WRITE, "Error writing message file",
#define CLEANUP_FLAG_HOLD (1<<2) /* Place message on hold */
#define CLEANUP_FLAG_DISCARD (1<<3) /* Discard message silently */
#define CLEANUP_FLAG_BCC_OK (1<<4) /* Ok to add auto-BCC addresses */
+#define CLEANUP_FLAG_NEED_DATE (1<<5) /* Require (Resent:-)Date: */
+#define CLEANUP_FLAG_NEED_FROM (1<<6) /* Require (Resent:-)From: */
+#define CLEANUP_FLAG_NEED_MSGID (1<<7) /* Require (Resent:-)Message-Id: */
+#define CLEANUP_FLAG_NEED_RCVD (1<<8) /* Require two Received: headers */
/*
* These are set on the fly while processing SMTP envelopes or message
* content.
*/
#define CLEANUP_FLAG_MASK_EXTRA \
- (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD)
+ (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD | CLEANUP_FLAG_NEED_DATE | \
+ CLEANUP_FLAG_NEED_FROM | CLEANUP_FLAG_NEED_MSGID | \
+ CLEANUP_FLAG_NEED_RCVD)
/*
* Diagnostics.
#define CLEANUP_STAT_SIZE (1<<2) /* Message file too big */
#define CLEANUP_STAT_CONT (1<<3) /* Message content rejected */
#define CLEANUP_STAT_HOPS (1<<4) /* Too many hops */
+#define CLEANUP_STAT_MISS_HDR (1<<5) /* Some missing header */
#define CLEANUP_STAT_RCPT (1<<6) /* No recipients found */
/*
abcdefghijklmnopqrstuvwxyz{|}~"
extern char *var_smtpd_exp_filter;
+#define REQUIRE_DATE_HDR "require_date_header"
+#define REQUIRE_FROM_HDR "require_from_header"
+#define REQUIRE_MSGID_HDR "require_message_id_header"
+#define REQUIRE_RCVD_HDR "require_received_header"
+
/*
* Heuristic to reject unknown local recipients at the SMTP port.
*/
* Patches change the patchlevel and the release date. Snapshots change the
* release date only, unless they include the same bugfix as a patch release.
*/
-#define MAIL_RELEASE_DATE "20030424"
+#define MAIL_RELEASE_DATE "20030519"
#define VAR_MAIL_VERSION "mail_version"
#define DEF_MAIL_VERSION "2.0.9-" MAIL_RELEASE_DATE
vstring_str(state->scratch));
if ((next_rcpt = send_rcpt + 1) == request->rcpt_list.len)
next_state = DEL_REQ_TRACE_ONLY(request->flags) ?
- LMTP_STATE_ABORT : LMTP_STATE_DATA;
+ LMTP_STATE_RSET : LMTP_STATE_DATA;
break;
/*
#define NULL_SERVER_ADDR ((char *) 0)
#define NULL_CLIENT_ADDR ((char *) 0)
- if (SASL_CLIENT_NEW("smtp", state->session->host,
+ if (SASL_CLIENT_NEW("lmtp", state->session->host,
NULL_CLIENT_ADDR, NULL_SERVER_ADDR,
state->sasl_callbacks, NULL_SECFLAGS,
(sasl_conn_t **) &state->sasl_conn) != SASL_OK)
#include <tok822.h>
#include <mail_params.h>
#include <bounce.h>
+#include <defer.h>
/* Application-specific. */
break;
}
}
+ if (vstream_ferror(fp))
+ status = defer_append(BOUNCE_FLAGS(state.request),
+ BOUNCE_ATTR(state.msg_attr),
+ "error reading .forward file: %m");
vstring_free(buf);
return (status);
}
static void post_jail_init(char *unused_name, char **unused_argv)
{
- const char *sep = " \t\r\n";
+ const char *sep = ", \t\r\n";
char *saved_filter;
char *bp;
char *type_name;
} else if ((state->err & CLEANUP_STAT_RCPT) != 0) {
qmqpd_reply(state, DO_LOG, QMQPD_STAT_HARD,
"Error: no recipients specified");
+ } else if ((state->err & CLEANUP_STAT_MISS_HDR) != 0) {
+ qmqpd_reply(state, DO_LOG, QMQPD_STAT_HARD,
+ "Error: missing message header");
} else {
qmqpd_reply(state, DO_LOG, QMQPD_STAT_RETRY,
"Error: internal error %d", state->err);
} else if ((state->err & CLEANUP_STAT_WRITE) != 0) {
state->error_mask |= MAIL_ERROR_RESOURCE;
smtpd_chat_reply(state, "451 Error: queue file write error");
+ } else if ((state->err & CLEANUP_STAT_MISS_HDR) != 0) {
+ state->error_mask |= MAIL_ERROR_POLICY;
+ smtpd_chat_reply(state, "550 Error: missing message header");
} else {
state->error_mask |= MAIL_ERROR_SOFTWARE;
smtpd_chat_reply(state, "451 Error: internal error %d", state->err);
/* Reject, defer or permit the request unconditionally. This is to be used
/* at the end of a restriction list in order to make the default
/* action explicit.
+/* .IP require_date_header
+/* .IP require_from_header
+/* .IP require_message_id_header
+/* .IP require_received_header
+/* Reject the message when it does not contain a Date: etc.
+/* message header. Only the Date: header is required by mail
+/* standards. The other headers are usually added by MTAs.
/* .IP reject_unknown_client
/* Reject the request when the client hostname could not be found.
/* The \fIunknown_client_reject_code\fR configuration parameter
DEFER_IF_REJECT2(state, MAIL_ERROR_POLICY,
"450 <%s>: %s rejected: defer_if_reject requested",
reply_name, reply_class);
+ } else if (strcasecmp(name, REQUIRE_DATE_HDR) == 0) {
+#ifndef TEST
+ rec_fprintf(state->dest->stream, REC_TYPE_FLGS, "%d",
+ CLEANUP_FLAG_NEED_DATE);
+#endif
+ } else if (strcasecmp(name, REQUIRE_FROM_HDR) == 0) {
+#ifndef TEST
+ rec_fprintf(state->dest->stream, REC_TYPE_FLGS, "%d",
+ CLEANUP_FLAG_NEED_FROM);
+#endif
+ } else if (strcasecmp(name, REQUIRE_MSGID_HDR) == 0) {
+#ifndef TEST
+ rec_fprintf(state->dest->stream, REC_TYPE_FLGS, "%d",
+ CLEANUP_FLAG_NEED_MSGID);
+#endif
+ } else if (strcasecmp(name, REQUIRE_RCVD_HDR) == 0) {
+#ifndef TEST
+ rec_fprintf(state->dest->stream, REC_TYPE_FLGS, "%d",
+ CLEANUP_FLAG_NEED_RCVD);
+#endif
}
/*
#define DBM_NO_TRAILING_NULL /* XXX check */
#define USE_STATVFS
#define STATVFS_IN_SYS_STATVFS_H
+#define BROKEN_NON_BLOCKING_WRITE_SELECT
#endif
#if defined(IRIX5)
* When flushing a buffer, allow for partial writes. These can happen
* while talking to a network. Update the cached file seek position, if
* any.
+ *
+ * XXX Workaround for IRIX brain damage: select() indicates that a pipe is
+ * writable, but write() transfers zero bytes on non-blocking pipes. This
+ * means that there is no reasonable way to enforce write timeouts.
*/
for (data = (char *) bp->data, len = to_flush; len > 0; len -= n, data += n) {
if (stream->timeout)
stream->iotime = time((time_t *) 0);
if ((n = stream->write_fn(stream->fd, data, len, stream->timeout, stream->context)) <= 0) {
+#ifdef BROKEN_NON_BLOCKING_WRITE_SELECT
+ if (n == 0) {
+ msg_warn("%s: write() transfers 0 bytes on a writable descriptor!",
+ myname);
+ sleep(1);
+ continue;
+ }
+#endif
bp->flags |= VSTREAM_FLAG_ERR;
if (errno == ETIMEDOUT)
bp->flags |= VSTREAM_FLAG_TIMEOUT;