add minimal working program

to build, run make
This commit is contained in:
Jeremy Baxter 2024-02-09 10:26:06 +13:00
commit 06df44d51e
2 changed files with 307 additions and 0 deletions

26
Makefile Normal file
View file

@ -0,0 +1,26 @@
PREFIX = /usr/local
DC = ldc2
CFLAGS = -Oz
#CFLAGS = -O0 -g -d-debug -wi
LDFLAGS = -L-lcurl
OBJS = mal.o
all: mal
mal: ${OBJS}
${DC} ${LDFLAGS} -of=$@ ${OBJS}
mal.o: mal.d
${DC} ${CFLAGS} -c mal.d
clean:
rm -f mal ${OBJS}
install:
mkdir -p ${DESTDIR}${PREFIX}/bin
chmod +x mal
cp -f mal ${DESTDIR}${PREFIX}/bin/mal
.PHONY: all clean install

281
mal.d Normal file
View file

@ -0,0 +1,281 @@
/*
* mal - simple command line mail client
*
* Copyright (c) 2024 Jeremy Baxter. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
module mal;
static import std.file;
import core.runtime : Runtime;
import std.conv : to;
import std.getopt : getopt, GetOptException;
import std.path : baseName;
import std.stdio : stderr, writeln;
import std.string : fromStringz;
import std.typecons : No;
import std.net.curl : CurlCode, SMTP, ThrowOnError;
import std.net.isemail : isEmail, EmailStatus;
import etc.c.curl : CurlError;
string fromAddr; /* [1] */
string serverAddr; /* -a */
string fqdn; /* -F */
string[] mailHeaders; /* -H */
bool useSmtps; /* -k/-s */
string messageFile; /* -M */
string message; /* -m */
string realName; /* -n */
string passPhrase; /* -P */
string serverPort; /* -p */
string subject; /* -S */
string[] toAddrs; /* -t */
string authUser; /* -u */
bool showVersion; /* -V */
int
main(string[] args)
{
SMTP mail;
EmailStatus fromStatus;
string messageBuf, protocol;
CurlCode err;
fqdn = hostName() ~ ".local";
mailHeaders = toAddrs = [];
subject = "";
useSmtps = true;
authUser = message = messageFile = passPhrase =
realName = serverAddr = serverPort = null;
try {
import std.getopt : config;
void
securityHandler(string opt) @safe
{
if (opt == "k") {
useSmtps = false;
} else {
assert(opt == "s");
useSmtps = true;
}
}
getopt(args,
config.bundling,
config.caseSensitive,
"a", &serverAddr,
"F", &fqdn,
"H", &mailHeaders,
"k", &securityHandler,
"M", &messageFile,
"m", &message,
"n", &realName,
"P", &passPhrase,
"p", &serverPort,
"S", &subject,
"s", &securityHandler,
"t", &toAddrs,
"u", &authUser,
"V", &showVersion
);
} catch (GetOptException e) {
import std.algorithm : startsWith;
enforceDie(!e.msg.startsWith("Unrecognized option"),
"unknown option " ~ extractOpt(e));
enforceDie(!e.msg.startsWith("Missing value for argument"),
"missing argument for option " ~ extractOpt(e));
die(e.msg); /* catch-all */
}
if (showVersion) {
writeln("mal version 0.0.0");
return 0;
}
if (args.length != 2) {
stderr.writeln(
`usage: mal [-ksV] [-a server] [-F fqdn] [-H header] [-n name]
[-P passphrase] [-p port] [-S subject] [-t toaddress]
[-u username] {-M messagefile | -m message} fromaddress`);
return 1;
}
enforceDie(message || messageFile,
"email messsage has to be provided through -M or -m");
enforceDie(!(message && messageFile),
"flags -M and -m cannot both be supplied in one invocation");
fromAddr = args[1];
fromStatus = isEmail(fromAddr);
enforceDie(fromStatus.valid, "invalid email address: %s", fromAddr);
if (toAddrs.length == 0) {
warn("warning: -t not supplied, sending to " ~ fromAddr);
toAddrs = [fromAddr];
}
foreach (ref string addr; toAddrs) {
enforceDie(isEmail(addr).valid,
"invalid email address: %s", addr);
addr = "<" ~ addr ~ ">";
}
authUser = authUser ? authUser : fromStatus.localPart;
protocol = useSmtps ? "smtps" : "smtp";
serverAddr = serverAddr ? serverAddr : fromStatus.domainPart;
serverPort = serverPort ? serverPort : useSmtps ? "465" : "587";
if (!passPhrase) {
warn("warning: -P not supplied, using empty passphrase");
passPhrase = "";
}
try {
messageBuf =
messageFile ? std.file.read(messageFile).to!string()
: message;
} catch (std.file.FileException e) {
die(e.msg);
}
mail = SMTP(protocol ~ "://" ~
serverAddr ~
":" ~ serverPort ~
"/" ~ fqdn);
mail.mailFrom = "<" ~ fromAddr ~ ">";
mail.mailTo = toAddrs.to!(const(char)[][]);
mail.message = formHeaders() ~ messageBuf;
mail.setAuthentication(authUser, passPhrase);
err = mail.perform(No.throwOnError);
if (err != CurlError.ok) {
import etc.c.curl : curl_easy_strerror;
/* known common errors */
switch (err) {
case CurlError.send_error:
warn("Failed sending data to " ~ serverAddr);
writeln("If the server uses STARTTLS, please try to connect");
writeln("using regular SSL (-s) or using plain insecure SMTP");
writeln("(-k). If none of those work, check the SMTP/SMTPS");
writeln("port for your server and provide it with -p.");
return 1;
default:
die(curl_easy_strerror(err).fromStringz().idup());
break;
}
}
return 0;
}
private string
formHeaders() @safe
{
string headerBuf;
headerBuf ~= "Content-Type: text/plain;charset=UTF-8\n";
headerBuf ~= "Subject: " ~ subject ~ "\n";
headerBuf ~= "From: " ~
(realName ? `"` ~ realName ~ `" <` : "<") ~
fromAddr ~ ">\n";
headerBuf ~= "To: ";
foreach (string addr; toAddrs) {
headerBuf ~= addr ~ ", ";
}
headerBuf.length -= 2;
headerBuf ~= "\n";
foreach (string hdr; mailHeaders) {
headerBuf ~= hdr ~ "\n";
}
return headerBuf ~ "\n";
}
private string
hostName()
{
import core.stdc.stdlib : free, malloc;
import core.sys.posix.unistd : gethostname;
/* according to POSIX */
enum HOST_NAME_MAX = 256;
char *hp;
string name;
hp = cast(char *)malloc(HOST_NAME_MAX * char.sizeof);
gethostname(hp, HOST_NAME_MAX);
name = hp.fromStringz().idup();
free(hp);
return name;
}
private void
warn(string mesg)
{
stderr.writeln(baseName(Runtime.args[0]) ~ ": " ~ mesg);
}
private void
die(string mesg)
{
import core.stdc.stdlib : exit;
warn(mesg);
Runtime.terminate();
exit(1);
}
private void
enforceDie(A...)(bool cond, string fmt, A a)
{
import std.format : format;
if (!cond)
die(format(fmt, a));
}
private string
extractOpt(in GetOptException e) @safe
{
import std.regex : matchFirst;
return e.msg.matchFirst("-.")[0];
}