/* * 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 = []; useSmtps = true; authUser = message = messageFile = passPhrase = realName = serverAddr = serverPort = subject = 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 1.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 user] {-M file | -m message} fromaddress`); return 1; } enforceDie(message || messageFile, "a message has to be provided with -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 ~ ">"; } /* https://todo.sr.ht/~jeremy/mal/2 */ authUser = authUser ? authUser : fromAddr; 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 = ""; } if (!subject) { warn("warning: -S not supplied, using empty subject line"); subject = ""; } 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]; }