Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic SMTP recipient checking #233

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

schmonz
Copy link
Member

@schmonz schmonz commented Feb 5, 2022

Give mail admins some basic SMTP recipient-checking tools:

  1. An API for external programs to reject recipients (@jaysoffian's RCPTCHECK patch)
  2. A program that calls the API and provides the API, giving a way to list a sequence of recipient-checkers in control/rcptchecks (my qmail-rcptcheck program)
  3. Two such recipient-checkers: qmail-rcptcheck-realrcptto derived from Paul Jarc's qmail-realrcptto patch, and qmail-rcptcheck-badrcptto derived from Ward Vandewege's badrcptto patch (was GPL'd, now CC0 -- thanks @cure!)

Notably not included:

  • Any other recipient-checkers, such as rejectutils' qmail-rcptcheck-qregex (never needed it myself) or something for vpopmail users
  • Enabling any of this by default (we probably should, in some future release)
  • Adapting RCPTCHECK to qmail-qmtpd (probably straightforward, just needs someone who cares)
  • Automated tests (I've run the code in production for years; still, sorry about this, and happy to add tests later)
  • All the other programmability of qmail-spp (but qmail-rcptcheck will happily run in that environment and recipient-checkers won't need to change at all)

Jay Soffian and others added 8 commits February 2, 2022 15:27
Original: https://www.soffian.org/downloads/qmail/qmail-smtpd.patch

This patch modifies qmail-smtpd to check for an environment variable,
$RCPTCHECK. If it is set, for each 'rcpt to:' that qmail-smtpd receives,
it will fork/exec $RCPTCHECK. $RCPTCHECK is run in the same environment
as qmail-smtpd (Note that qmail-smtpd changes its CWD to qmail-home
[typically /var/qmail] at startup). Additionally $SENDER is set to the
envelope from (mail from:) and $RECIPIENT is set to the envelope
recipient (for the current rcpt to:). Based on the return code (exit
value) of $RCPTCHECK, the rcpt to: address will either be accepted or
rejected as follows:

- 100: recipient is rejected with "553 sorry, no mailbox here by that
  name. (#5.1.1)"
- 111: connection is dropped with a temporary error "421 unable to
  verify recipient (#4.3.0)"
- 120: connection is dropped with a temporary error "421 unable to
  execute recipient check (#4.3.0)"
- All others: recipient is accepted.

120 is used internally (by the patch) if $RCPTCHECK cannot be executed.

Useful for validating recipient to addresses with an arbitrary program
(such as fastforward in '-n' mode).
Original: qmail-1.03-realrcptto-2006.12.10.patch

The qmail-realrcptto patch copies logic from qmail-send, qmail-lspawn,
qmail-getpw, and qmail-local into qmail-smtpd and qmail-qmtpd, so that
if a local delivery (i.e., one for a domain in /var/qmail/control/locals
or virtualdomains) would eventually bounce due to a missing .qmail file,
then that recipient address is rejected during the SMTP or QMTP protocol
conversation. There are other qmail patches around that do similar jobs
(badrcptto, etc.); the focus of this patch is to get this functionality
with no additional administrative effort, rather than qmail-smtpd's
running speed. You just set up your .qmail files, as you would have to
do anyway, and the rest is automatic, though slower than a CDB lookup.

Addresses which use the default delivery instructions are never rejected
by this patch, because they would never be bounced due to the lack of a
.qmail file.

This patch is less effective when there are .qmail-default files, or
when qmaild does not have sufficient filesystem permissions to stat()
users' .qmail files, since in those cases, qmail-smtpd cannot know that
the delivery would later bounce due to the lack of a .qmail file. For
example, I'm told that this patch is not useful for virtual domains
managed with vpopmail, since an applicable .qmail-default file always
exists. (I'm not familiar with vpopmail myself.) The patch may still be
useful for other local or virtual domains on the same server, if they
are not managed by vpopmail.
- Remove realrcptto references from qmail-qmtpd(8) and qmail-smtpd(8)
- Add qmail-rcptcheck-realrcptto(8) program and manual page
- Adjust realrcptto.c messages for the new context

Note that this leaves qmail-qmtpd(8) without realrcptto functionality.
Porting the RCPTCHECK patch would probably do the trick.
qmail-rcptcheck runs an administrator-defined sequence of programs to
check SMTP envelope senders and recipients. Checks must adhere to the
RCPTCHECK interface. If any check rejects, the message is rejected.

qmail-rcptcheck is most commonly invoked from qmail-smtpd via RCPTCHECK,
but also runs under qmail-spp (not documented here), so if and when we
switch to that, recipient-checking programs won't need to be changed.
Original: badrcptto-v1.02.netqmail.patch

The badrcptto patch SMTP-rejects email for specific recipients in an
email domain you otherwise accept mail for. List these "bad recipients"
in control/badrcptto, one per line.

Rejection occurs during the smtp envelope (rcpt) phase, before the email
headers and body can be transmitted.

Alex Kramarov added code to skip badrcptto checks if RELAYCLIENT is
set, so that one could prevent remote users from sending email to
certain local accounts while permitting local users to do so. This has
been merged.

Andrew McCarthy added logging for badrcptto hits. This has also
been merged.
@schmonz schmonz linked an issue Feb 5, 2022 that may be closed by this pull request
@schmonz schmonz added this to the 1.09 milestone Feb 5, 2022
@schmonz schmonz added the enhancement New feature or request label Feb 5, 2022
@lgtm-com
Copy link

lgtm-com bot commented Feb 5, 2022

This pull request introduces 1 alert when merging 44a63b4 into a943036 - view on LGTM.com

new alerts:

  • 1 for Comparison result is always the same

Comment on lines +28 to +31
extern void die_nomem();
extern void die_control();
extern void die_cdb();
extern void die_sys();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't they in realrcptto.h? They also need _noreturn annotations.

Comment on lines +5 to +7
void accept_recipient() { _exit( 0); }
void reject_recipient() { _exit(100); }
void unable_to_verify() { _exit(111); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should be annotated __noreturn.

Comment on lines +14 to +15
char *(*get_sender)();
char *(*get_recipient)();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type should be const to make it clear that the caller may not modify the value.

}

static int looks_like_spp() {
char *x = env_get("SMTPRCPTTO");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be const as well.


switch(pid = fork()) {
case -1:
rcptcheck.unable_to_execute();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callbacks are expected to be noreturn here, otherwise one would fallthrough. Without annotations this will lead to compiler warnings.

I wonder if it isn't less error prone to expect the callbacks to return and instead exit in the caller. That way an unexpected return from such a callback will not screw up the program logic.

- Remove references from qmail-smtpd(8)
- Extract logic to badrcptto.c
- Add qmail-rcptcheck-badrcptto(8) program and manual page
@lgtm-com
Copy link

lgtm-com bot commented Feb 5, 2022

This pull request introduces 1 alert when merging 1ef1c15 into a943036 - view on LGTM.com

new alerts:

  • 1 for Comparison result is always the same

Copy link

@josuah josuah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great addition, enters the game of providing an API, and a simple and sane one, for making patches unneeded. Kudos for bringing that up.

Letting you adjust the __noreturn and char -> char const as proposed by @DerDakon.

I will open an issue for the other points I discussed.

Thank you!

static void _badrcptto_log_rejection(char *recipient)
{
char smtpdpid[32];
char *remoteip = env_get("TCPREMOTEIP");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is best between using env_get or getenv for new code?

The getenv() function shall search the environment of the calling process for the environment variable name if it exists and return a pointer to the value of the environment variable. If the specified environment variable cannot be found, a null pointer shall be returned.

env_get returns the value of the first variable whose name is name, or 0 if there is no such variable.

Eventually, env_get can be #defined as getenv() in env.h.

case -1:
rcptcheck.unable_to_execute();
case 0:
execv(*rcptcheck_program,rcptcheck_program);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second the use of execv instead of execvp as it is a path relative to control.

Comment on lines +47 to +52
rcptcheck.get_sender = rcptcheck_get_sender;
rcptcheck.get_recipient = rcptcheck_get_recipient;
rcptcheck.accept_recipient = rcptcheck_accept_recipient;
rcptcheck.reject_recipient = rcptcheck_reject_recipient;
rcptcheck.unable_to_verify = rcptcheck_unable_to_verify;
rcptcheck.unable_to_execute = rcptcheck_unable_to_execute;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overhead of assigning function pointers might be extremely small, great way to do it!


if (looks_like_spp()) run_rcptchecks_under_spp();
else run_rcptchecks_under_rcptcheck();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, we should not have any empty function pointer.
Checking... Each run_rcptchecks_under_* sets 6 function pointers, the struct has 6 function pointers, run in an if-else... Function pointers not called before... Looks all good.

return ((j < address.len) && (constmap(&map,address.s + j,address.len - j - 1)));
}

static int _badrcptto_reject_string(char *string)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A char * string passed without a length looks suspicious: would char *string be written to without a length? Of course not, it is only read.

Making it char const * saves the time it takes to check it, and make it more obvious that the function returns 0 or 1 depending on string.

char smtpdpid[32];
char *remoteip = env_get("TCPREMOTEIP");
if (!remoteip) remoteip = "unknown";
str_copy(smtpdpid + fmt_ulong(smtpdpid,getppid())," ");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question with str_copy vs strcpy.

Comment on lines +37 to +38
stralloc addr = {0};
stralloc brt = {0};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I missed when those two are set stralloc_free(). Or does it not matter since it is a short-lived program?

I realize that there is not even any stralloc_free() in DJB code. It could be introduced later.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefits of using stralloc are as follows: [...]

  • If every heap-allocated object is represented by a stralloc, then it is very easy to identify what pointer is in the heap. When you stop using a pointer p, should you free it ? Sometimes it's not easy to find out. When you stop using a stralloc sa, you know you must call stralloc_free(&sa). Store your strong references as strallocs and weak references as simple pointers, and never free simple pointers. This policy allows me to boast that no skarnet.org software has ever leaked memory. -- https://skarnet.org/software/skalibs/libstddjb/stralloc.html

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other wraparound of DJB code do include stralloc_free, like fefe's https://www.fefe.de/libowfat/:

#include <stdlib.h>
#include "stralloc.h"

void stralloc_free(stralloc *sa) {
  if (sa->s) free(sa->s);
  sa->s=0;
  sa->a=sa->len=0;
}

@schmonz schmonz modified the milestones: 1.09, 1.90 Sep 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Configurable validation of SMTP recipients
3 participants