add minimal working program
to build, run make
This commit is contained in:
commit
06df44d51e
2 changed files with 307 additions and 0 deletions
26
Makefile
Normal file
26
Makefile
Normal 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
281
mal.d
Normal 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];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue