Commit 7542e4b1 authored by sikeda's avatar sikeda
Browse files

[feature] [experimental] "smtpc" is a command line utility aiming to be an...

[feature] [experimental] "smtpc" is a command line utility aiming to be an alternative to sendmail(1) utility and its clones with smaller footprint. (Note that this utility needs SMTP/LMTP server realying submitted messages).

It also supports some SMTP extensions several clones (and partly original one) have not been supported:
- DSN extension - As of Sympa 6.2, message tracking feature requires it.
- SMTPUTF8 extension - As of planned Sympa 7.0, email address internalization (eai) feature presumes it.

To use smtpc as replacement of sendmail, add sympa.conf the lines:
  sendmail      /path/to/smtpc
  sendmail_args --esmtp <host name of relaying server>
or with LMTP server:
  sendmail      /path/to/smtpc
  sendmail_args --lmtp <socket path of relaying server>


git-svn-id: https://subversion.renater.fr/sympa/branches/sympa-6.2-branch@12172 05aa8bb8-cd2b-0410-b1d7-8918dfa770ce
parent 26c1794d
......@@ -20,7 +20,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SUBDIRS = src default doc po www
SUBDIRS = src default doc po www src/smtpc
check_SCRIPTS = t/Language.t \
t/LockedFile.t \
......
......@@ -583,4 +583,5 @@ AC_CONFIG_FILES([
src/sbin/Makefile
www/Makefile
])
AC_CONFIG_SUBDIRS([src/smtpc])
AC_OUTPUT
# $Id$
ACLOCAL_AMFLAGS = -I m4
sbin_PROGRAMS = smtpc
smtpc_SOURCES = \
smtpc.c \
sockstr.c \
sockstr.h \
utf8.c \
utf8.h
dist_man1_MANS = smtpc.1
CLEANFILES = $(sbin_PROGRAMS) *~ *.bak core.*
smtpc.o: sockstr.h utf8.h
# -*- Autoconf -*-
# $Id$
AC_PREREQ([2.59])
AC_INIT([smtpc], [0.1a.1], [sympa-authors@listes.renater.fr])
AM_INIT_AUTOMAKE([foreign -Wall -Werror 1.9 tar-pax])
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
AC_CHECK_HEADERS([fcntl.h netdb.h stdlib.h string.h sys/socket.h unistd.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_TYPE_SIZE_T
AC_TYPE_SSIZE_T
AC_CHECK_SIZEOF([unsigned int])
AC_CHECK_SIZEOF([unsigned long])
# Checks for library functions.
AC_FUNC_MALLOC
AC_FUNC_REALLOC
AC_CHECK_FUNCS([memset select socket strdup strerror])
AC_FUNC_SNPRINTF
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
dnl @synopsis AC_FUNC_SNPRINTF
dnl
dnl Checks for a fully C99 compliant snprintf, in particular checks
dnl whether it does bounds checking and returns the correct string
dnl length; does the same check for vsnprintf. If no working snprintf
dnl or vsnprintf is found, request a replacement and warn the user
dnl about it. Note: the mentioned replacement is freely available and
dnl may be used in any project regardless of it's licence (just like
dnl the autoconf special exemption).
dnl
dnl @category C
dnl @author Rüdiger Kuhlmann <info@ruediger-kuhlmann.de>
dnl @version 2002-09-26
dnl @license AllPermissive
AC_DEFUN([AC_FUNC_SNPRINTF],
[AC_CHECK_FUNCS(snprintf vsnprintf)
AC_MSG_CHECKING(for working snprintf)
AC_CACHE_VAL(ac_cv_have_working_snprintf,
[AC_TRY_RUN(
[#include <stdio.h>
int main(void)
{
char bufs[5] = { 'x', 'x', 'x', '\0', '\0' };
char bufd[5] = { 'x', 'x', 'x', '\0', '\0' };
int i;
i = snprintf (bufs, 2, "%s", "111");
if (strcmp (bufs, "1")) exit (1);
if (i != 3) exit (1);
i = snprintf (bufd, 2, "%d", 111);
if (strcmp (bufd, "1")) exit (1);
if (i != 3) exit (1);
exit(0);
}], ac_cv_have_working_snprintf=yes, ac_cv_have_working_snprintf=no, ac_cv_have_working_snprintf=cross)])
AC_MSG_RESULT([$ac_cv_have_working_snprintf])
AC_MSG_CHECKING(for working vsnprintf)
AC_CACHE_VAL(ac_cv_have_working_vsnprintf,
[AC_TRY_RUN(
[#include <stdio.h>
#include <stdarg.h>
int my_vsnprintf (char *buf, const char *tmpl, ...)
{
int i;
va_list args;
va_start (args, tmpl);
i = vsnprintf (buf, 2, tmpl, args);
va_end (args);
return i;
}
int main(void)
{
char bufs[5] = { 'x', 'x', 'x', '\0', '\0' };
char bufd[5] = { 'x', 'x', 'x', '\0', '\0' };
int i;
i = my_vsnprintf (bufs, "%s", "111");
if (strcmp (bufs, "1")) exit (1);
if (i != 3) exit (1);
i = my_vsnprintf (bufd, "%d", 111);
if (strcmp (bufd, "1")) exit (1);
if (i != 3) exit (1);
exit(0);
}], ac_cv_have_working_vsnprintf=yes, ac_cv_have_working_vsnprintf=no, ac_cv_have_working_vsnprintf=cross)])
AC_MSG_RESULT([$ac_cv_have_working_vsnprintf])
if test x$ac_cv_have_working_snprintf$ac_cv_have_working_vsnprintf != "xyesyes"; then
AC_LIBOBJ(snprintf)
AC_MSG_WARN([Replacing missing/broken (v)snprintf() with version from http://www.ijs.si/software/snprintf/.])
AC_DEFINE(PREFER_PORTABLE_SNPRINTF, 1, "enable replacement (v)snprintf if system (v)snprintf is broken")
fi])
.TH SMTPC 1 "17 May 2015" 0.1a.1 "smtpc 0.1a.1"
.SH NAME
smtpc \- SMTP / LMTP client
.SH SYNOPSIS
smtpc --esmtp \fIhost\fR:\fIport\fR -f \fIenvelope@sen.der\fR
[ \fIoptions\fR... ] [ -- ] \fIrecipient@add.ress\fR ...
smtpc --lmtp \fIpath\fR -f \fIenvelope@sen.der\fR
[ \fIoptions\fR... ] [ -- ] \fIrecipient@add.ress\fR ...
.SH DESCRIPTION
\fBsmtpc\fP is a mail client.
It reads a message from standart input and submits it to mail server through
socket.
.SS Options
Any options not listed here are simply ignored.
.TP
\fB--dump\fP
Show dialog in the session.
.TP
\fB--esmtp\fP \fIhost\fR[:\fIport\fR]
Use TCP socket and ESMTP protocol to submit message.
Either this option or \fB--lmtp\fP option is required.
If \fIhost\fR is the IPv6 address, it must be enclosed in [...]
to avoid confusion with colon separating \fIhost\fR and \fIport\fR,
e.g. "[::1]".
.TP
\fB-f\fP \fIenvelope@sen.der\fR, \fB-f\fP\fIenvelope@sen.der\fR
Specifys envelope sender.
This option is required.
To specify "null envelope sender", use a separate empty argument or "<>".
.TP
\fB--iam\fP \fIhost.name\fR
Specifys host name or IP address literal used in EHLO request.
Default is "localhost".
.TP
\fB--lmtp\fP \fIpath\fR
Use Unix domain socket and LMTP protocol to submit message.
Either this option or \fB--esmtp\fP option is required.
.TP
\fB-N\fP \fIdsn\fR, \fB-N\fP\fIdsn\fR
Controls delivery status notification.
\fIdsn\fR may be single word "NEVER" or one or more of words "SUCCESS",
"FAILURE" and "DELAY" separated by comma.
If this option is not given, delivery status notification will be controlled
by server.
.TP
\fB--smtputf8\fP
Enables support for SMTPUTF8 extension.
\fBsmtpc\fR detects valid UTF-8 sequence in envelope and message header,
then requests this extension as neccessity.
.TP
\fB-V\fP \fIenvid\fR, \fB-N\fP\fIenvid\fR
Specifys envelope ID.
.TP
\fB--verbose\fP
Output the last response from the server to standard output.
.TP
\fB--\fP
Ends options.
Remainder of command line arguments are concidered to be recipient addresses.
.TP
\fIrecipent@add.ress\fR ...
Recipients to whom the message would be delivered.
.SS Exit status
.TP
\fB0\fR
Message was successfully submitted.
.TP
\fB253\fR
Message was rejected by server.
.TP
\fB254\fR
The server returns malformed or illegal response.
.TP
\fB255\fR
Network error occurred.
.SS "SMTP extensions"
\fBsmtpc\fR supports following extensions.
.TP
\fB8-bit MIME Transport\fR (RFC 6152)
\fBsmtpc\fR requests this extension if message contains octets with high bit.
.TP
\fBDelivery Status Notification\fR (RFC 3461)
See \fB-N\fR and \fB-V\fR options.
.TP
\fBMessage Size Declaration\fR (RFC 1870)
Estimated size of the message is informed to the server.
.TP
\fBInternationalized Email\fR (RFC 6531)
Experimentally supported.
See \fB--smtputf8\fR option.
.SH LIMITATIONS
\fBsmtpc\fR provides the feature of SMTP / LMTP client submitting messages
to particular server.
It will never provide extensive feature such as message queuing, retry after
temporary failure, mail routing using MX DNS record and so on.
Once the server rejects delivery, \fBsmtpc\fR exits and message is discarded.
.SH KNOWN BUGS
.TP 2
*
If NUL octets (\\0) are included in messages, they are transmitted to the
server.
.SH "SEE ALSO"
sendmail(1)
.SH HISTORY
\fBsmtpc\fP was initially written for Sympa project by
IKEDA Soji <ikeda@conversion.co.jp>.
/* $Id$ */
/*
* Sympa - SYsteme de Multi-Postage Automatique
*
* Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
* Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
* 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
* Copyright (c) 2011, 2012, 2013, 2014, 2015 GIP RENATER
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* smtpc was originally written by IKEDA Soji <ikeda@conversion.co.jp>
* for Sympa project.
*
* 2015-05-17 IKEDA Soji: Initial checkin to source repository.
*/
#include "config.h"
#include <signal.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "sockstr.h"
#include "utf8.h"
#define SMTPC_BUFSIZ (8192)
#define SMTPC_ERR_SOCKET (-1)
#define SMTPC_ERR_PROTOCOL (-2)
#define SMTPC_ERR_SUBMISSION (-3)
#define SMTPC_7BIT (0)
#define SMTPC_8BIT (1)
#define SMTPC_UTF8 (2)
#define SMTPC_PROTO_ESMTP (1)
#define SMTPC_PROTO_LMTP (2)
#define SMTPC_EXT_8BITMIME (1)
#define SMTPC_EXT_AUTH (1 << 1)
#define SMTPC_EXT_DSN (1 << 2)
#define SMTPC_EXT_PIPELINING (1 << 3)
#define SMTPC_EXT_SIZE (1 << 4)
#define SMTPC_EXT_SMTPUTF8 (1 << 5)
#define SMTPC_EXT_STARTTLS (1 << 6)
#define SMTPC_NOTIFY_NEVER (1)
#define SMTPC_NOTIFY_SUCCESS (1 << 1)
#define SMTPC_NOTIFY_FAILURE (1 << 2)
#define SMTPC_NOTIFY_DELAY (1 << 3)
static char buf[SMTPC_BUFSIZ];
static sockstr_t *sockstr;
static char *respbuf = NULL;
static size_t resplen = 0;
static struct {
int dump;
int verbose;
int protocol;
char *myname;
char *nodename;
char *servname;
char *path;
char *sender;
unsigned int notify;
char *envid;
int smtputf8;
} options = {
0, 0, 0, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0};
static struct {
char **recips;
int recipnum;
char *buf;
size_t buflen;
int envfeature;
int headfeature;
int bodyfeature;
} message = {
NULL, 0, NULL, 0, 0, 0, 0};
static struct {
unsigned long extensions;
} server = {
0};
static char *encode_xtext(unsigned char *str)
{
unsigned char *p;
char *encbuf, *q;
size_t enclen = 0;
p = str;
while (*p != '\0') {
if (*p == '+' || *p == '=')
enclen += 3;
else if (33 <= *p && *p <= 126)
enclen++;
else
enclen += 3;
p++;
}
encbuf = malloc(enclen + 1);
if (encbuf == NULL)
return NULL;
p = str;
q = encbuf;
while (*p != '\0') {
if (*p == '+' || *p == '=')
q += sprintf(q, "+%02X", (unsigned int) *p);
else if (33 <= *p && *p <= 126)
*q++ = *p;
else
q += sprintf(q, "+%02X", (unsigned int) *p);
p++;
}
*q = '\0';
return encbuf;
}
static ssize_t parse_options(int argc, char *argv[])
{
int i;
char *arg, *relaystr, *p;
options.dump = 0;
options.verbose = 0;
options.protocol = 0;
options.myname = "localhost";
options.nodename = NULL;
options.servname = "25";
options.sender = NULL;
options.notify = 0;
options.envid = NULL;
options.smtputf8 = 0;
for (i = 1; i < argc; i++) {
arg = argv[i];
if (arg[0] != '-')
break;
else if (arg[0] == '-' && arg[1] == '-') {
if (arg[2] == '\0') {
i++;
break;
} else if (strcmp(arg, "--dump") == 0) {
options.dump = 1;
continue;
} else if (strcmp(arg, "--esmtp") == 0 && i + 1 < argc) {
options.protocol = SMTPC_PROTO_ESMTP;
arg = argv[++i];
if (arg[0] == '[') {
p = options.nodename = arg + 1;
while (*p != '\0' && *p != ']')
p++;
if (*p == ']' && options.nodename < p)
*p++ = '\0';
else {
fprintf(stderr, "Malformed host %s\n", arg);
return -1;
}
if (*p != '\0')
options.servname = p;
} else {
p = options.nodename = arg;
while (*p != '\0' && *p != ':')
p++;
if (*p == ':' && options.nodename < p)
*p++ = '\0';
if (*p != '\0')
options.servname = p;
}
continue;
} else if (strcmp(arg, "--iam") == 0 && i + 1 < argc) {
options.myname = argv[++i];
continue;
} else if (strcmp(arg, "--lmtp") == 0 && i + 1 < argc) {
options.protocol = SMTPC_PROTO_LMTP;
options.path = argv[++i];
continue;
} else if (strcmp(arg, "--smtputf8") == 0) {
options.smtputf8 = 1;
continue;
} else if (strcmp(arg, "--verbose") == 0) {
options.verbose = 1;
continue;
} else {
fprintf(stderr, "Unknown option %s\n", arg);
return -1;
}
}
switch (arg[1]) {
case 'f':
if (arg[2] == '\0' && i + 1 < argc)
options.sender = argv[++i];
else if (arg[2] != '\0')
options.sender = arg + 2;
else
goto parse_options_novalue;
if (strcmp(options.sender, "<>") == 0)
options.sender += 2;
break;
case 'N':
if (arg[2] == '\0' && i + 1 < argc)
p = argv[++i];
else if (arg[2] != '\0')
p = arg + 2;
else
goto parse_options_novalue;
while (*p != '\0') {
char word[29], *wp;
wp = word;
while (*p == '\t' || *p == ' ' || *p == ',')
p++;
if (*p == '\0')
break;
while (*p != '\0' && *p != '\t' && *p != ' ' && *p != ','
&& wp - word + 1 < sizeof(word))
if ('a' <= *p && *p <= 'z')
*wp++ = *p++ + ('A' - 'a');
else
*wp++ = *p++;
*wp = '\0';
if (strcmp(word, "NEVER") == 0) {
options.notify |= SMTPC_NOTIFY_NEVER;
} else if (strcmp(word, "SUCCESS") == 0)
options.notify |= SMTPC_NOTIFY_SUCCESS;
else if (strcmp(word, "FAILURE") == 0)
options.notify |= SMTPC_NOTIFY_FAILURE;
else if (strcmp(word, "DELAY") == 0)
options.notify |= SMTPC_NOTIFY_DELAY;
else {
fprintf(stderr, "Unknown NOTIFY keyword %s\n", word);
return -1;
}
if (options.notify & SMTPC_NOTIFY_NEVER &&
options.notify & ~SMTPC_NOTIFY_NEVER) {
fprintf(stderr,
"NEVER keyword must not appear with other(s)\n");
return -1;
}
}
break;
case 'V':
if (arg[2] == '\0' && i + 1 < argc)
options.envid = argv[++i];
else if (arg[2] != '\0')
options.envid = arg + 2;
else
goto parse_options_novalue;
p = options.envid;
while (*p != '\0')
if (32 <= *p && *p <= 126)
p++;
else {
fprintf(stderr,
"ENVID contains illegal character \\x%02X\n",
*p);
return -1;
}
break;
default:
break;
parse_options_novalue:
fprintf(stderr, "No value for option %s\n", arg);
return -1;
}
}
message.recipnum = argc - i;
message.recips = argv + i;
if (options.protocol == 0) {
fprintf(stderr,
"Either --esmtp or --lmtp option must be given.\n");
return -1;
}
if (options.protocol == SMTPC_PROTO_ESMTP && options.nodename == NULL
|| options.protocol == SMTPC_PROTO_LMTP && options.path == NULL) {
fprintf(stderr, "Server socket is not specified.\n");
return -1;
}
if (options.sender == NULL) {
fprintf(stderr, "Envelope sender is not specified.\n");
return -1;
}
if (message.recipnum <= 0) {
fprintf(stderr, "No recipients are specified.\n");
return -1;
}
return message.recipnum;
}
static int check_utf8_address(char *addrbuf)
{
size_t len;
ssize_t rs;
len = strlen(addrbuf);
if (len == 0)
return SMTPC_7BIT;
rs = utf8_check((unsigned char *) addrbuf, len);
if (rs < 0)
return SMTPC_7BIT;
else if (rs < len)
return SMTPC_8BIT;
else
return SMTPC_UTF8;
}
static void check_message_features(void)
{
size_t i;
ssize_t rs;
message.envfeature = check_utf8_address(options.sender);
for (i